利用Trie树求多个字符串编辑距离的进一步优化

1.引言

题目的意思应该是:在一个给定的字典中,求与给定的字符串的编辑距离不大于2的所有的单词。原先写过两片关于此问题的文章,那两片篇章文章给出两种解决思路:其一是暴力求解法,这种方法最容易想到。就是将词典中的词一一与给定的字符串计算编辑距离,不大于2的输出,大于2的舍弃,这种方法思路简单但是很费时间。其二根据词典中这些词之间的编辑距离建立一个以单词为节点的Trie树,遍历的时候,通过计算根节点与给定字符串的编辑距离就可以排除掉一部分分支了,然后继续计算该字符串与剩余的分支的根的编辑距离,继续排除一部分……就这样通过不断的剪枝来达到逐步排除单词。总体上来讲,这种方法是通过排除节点,减少比较次数来达到节约时间的目的,相比暴力求解法,该方法能提高10倍的速度。有兴趣的读者请参阅这两篇文章:利用Trie树求多个字符串的最小编辑距离 
和   Trie树求多个字符串最小编辑距离的空间优化

这里再介绍一种方法,速度提高的更多。思路仍然是将词典中的单词建立Trie树,但是这里只是建立普通的Trie树,将每个字母作为一个节点。只是在遍历的时候讲究一些技巧,就可以在很少的时间内将所有与给定字符串编辑距离不大于2的单词找出来了。

通俗来讲,从Trie树树根到叶子是标识一个单词的完整路径一条完整的路径A,这条路径上有数个节点。假定给定的字符串也在Trie树中,那么也必然形成这个字符串的独特路径B。比如:Trie树中的单词abcde的路径:a -> b -> c -> d -> e,给定的字符串abrde形成的路径:a -> b -> r -> d -> e。如果A和B能够大部分重合,不重合的部分不大于2的话,那么就是符合我们要求的单词。刚才的单词abcde和abrde只有在字母r这里不重合,别处都重合,因此符合要求,应将abcde输出。

我们不能期望所有单词形成的路径都与给定的字符串重合(那样词典就蜕化为一个单词了),只要他们大部分重合就可以,这些单词都被赋予两项权利,允许他们在两处不重合,这些权利是:

(1)增加一个字符

(2)删除一个字符

(3)修改一个字符

(4)相邻两个字符交换

只要将给定的字符串经过上述的操作变换后与字典中的路径重合即可。注意:这四种方式一次只能使用一种,最多只能用两次。这也就是说,词典中单词与给定字符串的编辑距离不大于2。

明白了这一点,我们就可以构造算法,将给定的字符串经过最多两次变换后,检查词典中有没有对应的路径存在即可。由于上述的变换可能在给定的字符串的任意位置发生,因此要将所有的可能的情况都考虑到,所以用递归的思路来解决这个问题。具体的思路请直接参考源代码。

2.时间复杂度分析

测试环境:词典中词的长度在5~30之间,目标字符串长度为6,随机取10组数据进行测试进行测试。

测试结果:

序号 Trie树搜索时间 暴力匹配搜索时间 时间比值
1 3.68 796 0.46%
2 3.75 846 0.44%
3 3.8 810 0.47%
4 3.6 821 0.44%
5 3.61 810 0.45%
6 3.68 812 0.45%
7 3.6 832 0.43%
8 3.71 821 0.45%
9 3.61 816 0.44%
10 3.73 813 0.46%
平均值 3.677 817.7 0.45%

结果对比:

(1)本文结果:3.67ms        0.45%

(2)前文结果:64.4ms        7.90%

(3)暴力匹配:817.7ms      100%

将上表中的数据同前面文章中的数据进行对比,从上面的对比中可以看出本文的方法在性能上有了极大的提升。

3.源代码

#include <iostream>
#include <fstream>
#include <string>
#include <cstring>
#include <vector>
#include <algorithm>
#include <sys/time.h>

using namespace std;

const int SON_NUM = 26;		//树中每个节点分支的数量
const int MAX = 30;		//随机字符串的最大长度
const int Y = 30;		//字典中字符串的最大长度

/**
 *节点定义
 *
 */
class Node{
	public:
		char c;			//节点所代表的单词
		int flag;		//节点是否是单词的尾节点
		int count;		//词典中相同单词的数量
		string word;		//词典中的单词
		Node* son[SON_NUM];	//指向分支节点的指针
	public:
		Node() : flag(0), count(0), word(""){
			memset(son, NULL, sizeof(Node*)*SON_NUM);
		}
};

/*
 *Trie树的操作定义
 *
 */
class Trie{
	public:
		Node* pRoot;		//根节点
	public:
		Trie();
		~Trie();
		void print(Node* r);			//打印某节点
		void destory(Node* r);			//销毁某节点
		void insert(string str);		//插入单词
		bool search(string str, int &count);	//搜索单词(普通搜索)
		bool search_2(string str, int pos, Node* pRoot, vector<string> &word_set, int lev);	//搜索单词(特殊搜索)
};

//构造函数
Trie::Trie(){
	pRoot = new Node();
}

//析构函数
Trie::~Trie(){
	destory(pRoot);
}

//打印某个节点
void Trie::print(Node* r){
	if(r != NULL){
		cout << r -> c << endl;
		for(int i = 0; i < SON_NUM; i++){
			print(r -> son[i]);
		}
		cout << endl;
	}
}

//销毁某个节点
void Trie::destory(Node* r){
	if(NULL == r){
		return;
	}
	for(int i = 0; i < SON_NUM; i++){
		destory(r -> son[i]);
	}
	delete r;
	r = NULL;
}

//向Trie树插入单词
void Trie::insert(string str){
	int index = 0;
	Node* pNode = pRoot;
	//不断向下搜索单词的字符是否出现
	for(int i = 0; i < str.size(); i++){
		index = str[i] - 'a';
		//当字符在规定的范围内时,才进行检查
		if(index >= 0 && index <= SON_NUM){
			//父节点是否有指针指向本字符
			if(NULL == pNode -> son[index]){
				pNode -> son[index] = new Node();
			}
			//指针向下传递
			pNode = pNode -> son[index];
			pNode -> c = str[i];
		} else {
			return;
		}
	}
	//判断单词是否出现过
	if(pNode -> flag == 1){
		//单词已经出现过
		pNode -> count++;
	} else {
		//单词没有出现过
		pNode -> flag = 1;
		pNode -> count++;
		pNode -> word = str;
	}
}

//普通搜索:搜索指定单词,并返回单词出现次数(如果存在)
bool Trie::search(string str, int &count){
	Node* pNode = pRoot;
	int index = 0;
	int i = 0;
	while(pNode != NULL && i < str.size()){
		index = str[i] - 'a';
		if(index >= 0 && index < SON_NUM){
			//字符在指定的范围内时
			pNode = pNode -> son[index];
			i++;
		}else{
			//字符不再指定的范围内
			return false;
		}
	}
	//判断字符串是否出现过
	if(pNode != NULL && pNode -> flag == 1){
		count = pNode -> count;
		return true;
	}else{
		return false;
	}
}

//特殊搜索:在词典中搜索与给定字符串编辑距离不大于lev的单词
bool Trie::search_2(string str, int pos, Node* pNode, vector<string> &word_set, int lev){

	//节点不为空的情况下操作
	if(NULL != pNode){

		//1.增加字母:
		if(lev > 0 && pos < str.size() + 2){
			int pos_1 = pos;
			int lev_1 = lev;
			lev_1--;
			for(int j = 0; j < SON_NUM; j++){
				//增加任一个字母均可
				if((pos_1 < str.size() && j != (str[pos_1] - 'a') && NULL != pNode -> son[j]) ||
					(pos_1 >= str.size() && NULL != pNode -> son[j])){
					//Trie树中存在的任何一个字母都要顾及到
					Node* pCur_1 = pNode -> son[j];
					if(pos_1 == str.size() && NULL != pCur_1 && pCur_1 -> flag == 1){
						word_set.push_back(pCur_1 -> word);
					}
					//继续比较
					search_2(str, pos_1, pCur_1, word_set, lev_1);
				}
			}
		}

		//2.删除字母:
		if(lev > 0 && pos < str.size()){
			Node* pCur_2 = pNode;
			int pos_2 = pos;
			int lev_2 = lev;
			pos_2++;
			lev_2--;
			//继续比较
			if(pos_2 == str.size() && NULL != pCur_2 && pCur_2 -> flag == 1){
				word_set.push_back(pCur_2 -> word);
			}
			search_2(str, pos_2, pCur_2, word_set, lev_2);
		}

		//3.修改字母:
		if(lev > 0 && pos < str.size()){
			int pos_3 = pos;
			int lev_3 = lev;
			int index_3 = str[pos_3] - 'a';
			pos_3++;
			lev_3--;
			for(int j = 0; j < SON_NUM; j++){
				if(j != index_3 && NULL != pNode -> son[j]){
					Node* pCur_3 = pNode -> son[j];
					//继续比较
					if(pos_3 == str.size() && NULL != pCur_3 && pCur_3 -> flag == 1){
						word_set.push_back(pCur_3 -> word);
					}
					search_2(str, pos_3, pCur_3, word_set, lev_3);
				}
			}
		}

		//4.交换字母:
		if(lev > 0 && pos < str.size() - 1){
			Node* pCur_4 = pNode;
			int pos_4 = pos;
			int lev_4 = lev;
			lev_4--;
			int index_up = str[pos_4] - 'a';
			int index_down = str[pos_4+1] - 'a';
			if(NULL != pCur_4 -> son[index_down] &&
				NULL != pCur_4 -> son[index_down] -> son[index_up]){
				pCur_4 = pCur_4 -> son[index_down] -> son[index_up];
				pos_4 = pos_4 + 2;
				//继续比较
				if(pos_4 == str.size() && NULL != pCur_4 && pCur_4 -> flag == 1){
					word_set.push_back(pCur_4 -> word);
				}
				search_2(str, pos_4, pCur_4, word_set, lev_4);
			}
		}

		//5.直接匹配;
		if(pos < str.size() && NULL != pNode -> son[str[pos] - 'a']){
			Node* pCur_5 = pNode;
			int pos_5 = pos;
			int lev_5 = lev;
			int index_5 = str[pos_5] - 'a';
			pCur_5 = pCur_5 -> son[index_5];
			pos_5++;
			if(pos_5 == str.size() && NULL != pCur_5 && pCur_5 -> flag == 1){
				word_set.push_back(pCur_5 -> word);
			}
			search_2(str, pos_5, pCur_5, word_set, lev_5);
		}
	}
}

/*
 *辅助函数
 *
 */

//求两个字符串的最断编辑距离
int edit_length(string &x, string &y){
	int xlen = x.length();
	int ylen = y.length();
	int edit[3][Y+1];
	memset(edit, 0, sizeof(edit));

	int i = 0;
	int j = 0;
	for(j = 0; j <= ylen; j++){
		edit[0][j] = j;
	}
	for(i = 1; i <= xlen; i++){
		edit[i%3][0] = edit[(i-1)%3][0] + 1;
		for(j = 1; j <= ylen; j++){
			if (x[i-1] == y[j-1]) {
				edit[i%3][j] = min(min(edit[i%3][j-1] + 1, edit[(i-1)%3][j] + 1),
							edit[(i-1)%3][j-1]);
			} else {
				if(i >= 2 && j >= 2 && x[i-2] == y[j-1] && x[i-1] == y[j-2]){
					edit[i%3][j] = min(min(edit[i%3][j-1] + 1, edit[(i-1)%3][j] + 1),
										min(edit[(i-1)%3][j-1] + 1, edit[(i-2)%3][j-2] + 1));
				} else {
					edit[i%3][j] = min(min(edit[i%3][j-1] + 1, edit[(i-1)%3][j] + 1),
										edit[(i-1)%3][j-1] + 1);
				}
			}
		}
	}
	return edit[(i-1)%3][j-1];
}

//生成随机字符串
string rand_string(int len){
	srand(time(NULL));
	char a[MAX+1];
	for(int i = 0; i < len; i++){
		a[i] = rand()%26 + 'a';
	}
	a[len] = '\0';
	string str(a);
	return str;
}

//获取当前时间(微秒)
long getCurrentTime(){
	struct timeval tv;
	gettimeofday(&tv, NULL);
	return tv.tv_sec*1000*1000 + tv.tv_usec;
}

int main(){
	//1.定义对象
	Trie trie;
	string str;
	ifstream fin;

	//2.建立Trie树
	fin.open("dict.txt");
	if(!fin){
		cout << "open file fail!" << endl;
	}
	while(getline(fin, str, '\n')){
		trie.insert(str);
	}
	fin.close();

	/*
	//3.验证Trie树建立成功
	fin.open("dict.txt");
	if(!fin){
		cout << "open file fail!" << endl;
	}
	int count = 0;
	while(getline(fin, str, '\n')){
		bool isFind = trie.search(str, count);
		cout << count << "  " << str << endl;
	}
	fin.close();

	*/

	//3.产生随机字符串
	string rand_str = rand_string(6);
	cout << "随机字符串为:" << rand_str << endl;

	//4.利用Trie树计算结果
	vector<string> word_set_1;
	long time_3 = getCurrentTime();
	trie.search_2(rand_str, 0, trie.pRoot, word_set_1, 2);
	long time_4 = getCurrentTime();

	//消除重复元素
	sort(word_set_1.begin(), word_set_1.end());
	word_set_1.erase(unique(word_set_1.begin(), word_set_1.end()), word_set_1.end());

	//5.利用暴力匹配计算结果
	vector<string> word_set_2;
	vector<string> word_dict;
	fin.open("dict.txt");
	if(!fin){
		cout << "打开文件失败!" << endl;
	}
	while(getline(fin, str, '\n')){
		word_dict.push_back(str);
	}
	fin.close();
	int size = word_dict.size();
	long time_5 = getCurrentTime();
	for(int j = 0; j < size; j++){
		if(edit_length(word_dict[j], rand_str) < 3){
			word_set_2.push_back(word_dict[j]);
		}
	}
	long time_6 = getCurrentTime();
	fin.close();

	//6.结果比较
	sort(word_set_1.begin(), word_set_1.end());
	sort(word_set_2.begin(), word_set_2.end());

	cout << "word_set_1的大小:" << word_set_1.size() << endl;
	cout << "结果为:";
	for(int i = 0; i < word_set_1.size(); i++){
		cout << "  " << word_set_1[i];
	}
	cout << endl;

	cout << "word_set_2的大小:" << word_set_2.size() << endl;
	cout << "结果为:";
	for(int i = 0; i < word_set_2.size(); i++){
		cout << "  " << word_set_2[i];
	}
	cout << endl;

	if(word_set_1 == word_set_2){
		cout << "验证正确" << endl;
	} else {
		cout << "验证错误" << endl;
	}

	//7.时间比较
	//cout << "建立Trie树用时(微秒):" << time_2 - time_1 << endl;
	cout << "Trie树搜索用时(ms):" << double(time_4 - time_3)/1000 << endl;
	cout << "暴力搜索用时(ms):"   << double(time_6 - time_5)/1000 << endl;
	cout << "百分比:" << double(time_4 -time_3)/(time_6 - time_5) << endl;
}

利用Trie树求多个字符串编辑距离的进一步优化,布布扣,bubuko.com

时间: 2024-10-15 04:41:12

利用Trie树求多个字符串编辑距离的进一步优化的相关文章

【权值线段树】离散化介绍 (+利用 线段树 求逆序对)

先介绍一下离散化 桶排大家应该知道,就是开一个数组(下标为数值,记录了该数值的出现次数)然后遍历过去如果出现次数不为零,那就输出这些数字,理论时间复杂度可以达到O(N)但是由于内存限制,不能开很大的数组. 然而 如果某个数列中的数字不要求大小确定,只要求这些数字有相对的大小就够了的话,离散化就有了用武之地 举个例子:数列 3 8 7 5 2000000000000000 我们发现有几个数之间差距很大,但是我们用不到数值的大小,只要求相对大小,那怎么办呢? 观察下面的数列: 1 4 3 2 5 真

算法学习——trie树求最大异或对

这道题目很难想到是字典树,如果不是放在字典树单元的话.简单来说,一个整数,是可以转化成为一个32位的二进制数,而也就可以变成长度为32位的二进制字符串.既然如此的话,根据二进制异或的原理:不同得1,否则得0,假如想让这个数字最大,我们就需要使得这个数的二进制表示中从左往右尽可能的取到1,也就是尽量的使得1最多. 那么我们可以这么做: 每一次检索的时候,我们都走与当前AiAi这一位相反的位置走,也就是让Xor值最大,如果说没有路可以走的话,那么就走相同的路. 详情见代码: #include<ios

Trie树

Trie树,即字典树或单词查找树,主要用于大量字符串的检索.去重.排序等操作. 主要原理就是利用字符串的公共前缀建立一棵多叉树,牺牲空间换取时间. 1 //Trie树 2 #include <iostream> 3 #include <string> 4 using std::cin; 5 using std::cout; 6 using std::endl; 7 using std::string; 8 9 const int SIZE = 26; 10 const char B

剑指Offer——Trie树(字典树)

剑指Offer--Trie树(字典树) Trie树 Trie树,即字典树,又称单词查找树或键树,是一种树形结构,是一种哈希树的变种.典型应用是统计和排序大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计.它的优点是:最大限度地减少无谓的字符串比较,查询效率比哈希表高. Trie的核心思想是空间换时间.利用字符串的公共前缀来降低查询时间的开销以达到提高效率的目的. Trie树也有它的缺点,Trie树的内存消耗非常大.当然,或许用左儿子右兄弟的方法建树的话,可能会好点.可见,优

字典树Trie树

一.字典树 字典树--Trie树,又称为前缀树(Prefix Tree).单词查找树或键树,是一种多叉树结构. 上图是一棵Trie树,表示了关键字集合{"a", "to", "tea", "ted", "ten", "i", "in", "inn"} .从上图可以归纳出Trie树的基本性质: 1. 根节点不包含字符,除根节点外的每一个子节点都包含一

trie树信息抽取之中文数字抽取

这一章讲一下利用trie树对中文数字抽取的算法.trie树是一个非常有用的数据结构,可以应用于大部分文本信息抽取/转换之中,后续会开一个系列,对我在实践中摸索出来的各种抽取算法讲开来.比如中文时间抽取,地址抽取等. Trie树 trie树又称为前缀树,索引树,字典树.用来对字符串进行索引,每个节点存储一个字符,每个叶子节点代表一个字符串,即从根到它的路径上所有字符的序列. 这个结构有什么优点呢?可以快速的匹配一个目标字符串中存在的单词.换句话说,我有一个字典,是单词的集合,我把字典中所有的单词存

BZOJ 1212 HNOI 2004 L语言 Trie树

题目大意:给出一些单词,和一些句子,当且仅当句子可以分割成的子串都可以被词典翻译,就说明这个子串是可以被翻译的.求最长的可以被翻译的前缀长度. 思路:利用Trie树来刷数组,能够刷到的最长的地方就是这个串最长可以翻译到的地方. PS:在BZOJ上Trie居然比AC自动机快,我的渣代码都刷到第一篇了... CODE: #include <cstdio> #include <cstring> #include <iostream> #include <algorith

BZOJ 3439 Kpm的MC密码 Trie树+可持久化线段树

题目大意:给定n个字符串,对于每个字符串求以这个字符串为后缀的字符串中第k小的编号 首先将字符串反转 那么就变成了对于每个字符串求以这个字符串为前缀的字符串中第k小的编号 然后考虑对字符串排序 那么对于每个字符串以它为前缀的字符串一定是连续的 那么就转化成了区间第k小 这个用可持久化线段树可以解决 排序自然不能直接排 既然是字符串 考虑Trie树+DFS即可 注意字符串有重复的 小心 #include <vector> #include <cstdio> #include <

Codeforces 282E. Sausage Maximization【trie树(非指针版)】

题目大意: 给出一串数,pre[i](前i个数的异或)为a[0]~a[i-1]的异或,post[i](后缀的异或)为a[i]~a[n-1]的异或,求pre[i]^post[j]的最大值(0<=i<=j<=n),其中,pre[0]=0,post[n]=0(表示一个数都不选). 做法: 利用trie树将后缀或者前缀存储起来,首先从pre[n]开始,往前遍历,对于每个前缀,将此时的后缀添加到trie树中,再在trie中寻找与当前前缀异或之后能得到最大的值. 在trie中存储数的时候,将该数的二