又称单词查找树,Trie树,是一种树形结构,是一种哈希树的变种。查单词的话就有26个字母,我们把一个单词分成一个个字母来构成字典树。主要包括插入,查找,删除操作
例如:
只要在第二个p,k,e这3出做个记号就能分出这3个单词了。这就是字典树的思路。
例一:来个例题:
Sample Input
START from fiwo hello difh mars riwosf earth fnnvk like fiiwj END START difh, i'm fiwo riwosf. i fiiwj fnnvk! END |
Sample Output
hello, i'm from mars. i like earth! |
题意就是个把火星文转换为English输出,第一个START到END是字典,里面有提供的所有火星文->英文的转换,第二个START到END是要你翻译的句子,如果字典里面没有就原样输出。做法就是把字典的所有的火星文存储成一棵字典树,在要做标记的地方用英语来当标记,即是f->i->w->o,单词应该在o处终结,那么在o处存储from这个单词,其他字符就存个'*'表示没完结(不同的题标记方法可能不同)。
#include<iostream>
#include<string>
#include<cstdio>
#include<cstring>
using namespace std;
const int N=3010;
struct Tire
{
char dic[11];//用来做标记
Tire* next[26];//保存26个字母
};
Tire* head;
void init()//初始化字典树,主要是初始化根节点
{
head=new Tire;
head->dic[0]='*';
for(int i=0;i<26;i++)
head->next[i]=NULL;//初始化时一个火星文都没
}
void insert(char eng[],char mar[])
{
Tire* cur;
cur=head;
int len=strlen(mar);
for(int i=0;i<len;i++)
{
int temp=mar[i]-'a';//计算mar[i]是第几个字母
if(cur->next[temp]==NULL)
{
cur->next[temp]=new Tire;
cur=cur->next[temp];
cur->dic[0]='*';//还没到火星文完结,用‘*’来标记
for(int j=0;j<26;j++)
{
cur->next[j]=NULL;
}
}
else
cur=cur->next[temp];
}
strcpy(cur->dic,eng);//火星文完结,用英语单词来标记
}
string search(string mar)
{
int len=mar.size();
int temp;
Tire* cur=head;
for(int i=0;i<len;i++)
{
temp=mar[i]-'a';
if(cur->next[temp]==NULL)
return mar;//火星文没完就没后续字母,返回mar
cur=cur->next[temp];
}
if(cur->dic[0]=='*')
return mar;//火星文完结但不是用英语单词标记,例如apple火星文->kill英文,给你app翻译,虽然能找到app,但事实上字典并没这个词
else
return cur->dic;
}
void del(Tire* head)
{
for(int i=0;i<26;i++)
{
if(head->next[i]!=NULL)
del(head->next[i]);//用递归来释放内存,防止内存泄露
}
delete(head);
}
int main()
{
init();
char dic[11];
char eng[11],mar[11];//英文与火星文
char history[N];//待翻译文章
string word,answer;
word="";
answer="";
scanf("%s",dic);
while(scanf("%s",eng) && strcmp(eng,"END")!=0)
{
scanf("%s",mar);
insert(eng,mar);
}
scanf("%s",dic);
getchar();
while(gets(history) && strcmp(history,"END")!=0)
{
int len=strlen(history);
answer="";
for(int i=0;i<len;i++)
{
if(islower(history[i]))
word+=history[i];
else
{
answer+=search(word);
word="";
answer+=history[i];
}
}
cout<<answer<<endl;
}
del(head);
}
例二:又一典型题:统计难题
banana band bee absolute acm ba b band abc
2 3 1 0
题意输入一些单词,以空行表示输入结束,空行之后为查询,例如ba开头的有几个单词,明显2个,以b开头有3个,把这里的2和3就是作为输出结果。做法就是把这些输入单词存成一棵字典树,这题与上一题不同之处就在于标记的问题,这里的标记是int nCount=0;初始化为0,第一个b来了就++,第二个b来就再++,那样就记录了b有几个,能起到统计有几个开头的作用了。
#include<iostream>
#include<cstring>
#include<string>
using namespace std;
struct Trie
{
int nCount;
Trie *next[26];
Trie()//这里搞个构造函数就不用每申请次内存都要初始化,new会自动调用这个函数来初始化好给我们用
{
nCount=0;
for(int i=0;i<26;i++)
next[i]=NULL;
}
};
Trie *head=new Trie();
void Insert(string word)
{
Trie *cur=head;
int i,j;
for(i=0;i<word.size();i++)
{
int temp=word[i]-'a';
if(cur->next[temp]==NULL)//如果没有那个字母就new个
{
cur->next[temp]=new Trie();
cur=cur->next[temp];
}
else//否则直接进入下一个字母
{
cur=cur->next[temp];
}
cur->nCount++;//记得每次遇到一个字母就表示那个字母出现一次,就应该加1
}
}
int Search(string word)
{
Trie *cur=head;
int i,j;
for(i=0;i<word.size();i++)
{
int temp=word[i]-'a';
if(cur->next[temp]==NULL)//要查找的前缀没找完就断了
{
return 0;
}
cur=cur->next[temp];//一直找到前缀的最后一个字母才好输出
}
return cur->nCount;
}
void del(Trie *head)
{
for(int i=0;i<26;i++)
{
if(head->next[i]!=NULL)
del(head->next[i]);
}
delete head;
}
int main()
{
char str[12];
int i;
while(gets(str) && str[0])
Insert(str);
while(gets(str))
printf("%d\n",Search(str));
del(head);
}
例三:继续来例题,已经可以慢慢看得出代码有一部分是几乎一模一样的了,那就是字典树的模板了。
Immediate Decodability:
01 10 0010 0000 9 01 10 010 0000 9
Set 1 is immediately decodable Set 2 is not immediately decodable
题意是输入以9为分割点,为一组输入,一组输入中有好多01串,如果不存在共同前缀就输出is immediate......存在共同前缀就输出is not immed....就第二组输入中01和010就是有共同前缀,所以输出is not ...;做法就是同样建立一棵字典树保存一组输入的所有01串,在一个01串完结的时候做个标记,在插入新的01串的时候,如果遇到前面的01串的完结标记,那么就意味着这个在插入的01串与之前的串中有共同的前缀了,这时用一个全部变量记录一下这个组是有共同前缀的,在输入遇到'9'时就判断下全局变量记录状况来看输出什么。
#include<iostream>
#include<string>
using namespace std;
struct Trie
{
bool end;//来标记01串的完结,没完就false,完结就true
Trie *next[2];//用来连接串后面的01字符
Trie()
{
end=false;
next[0]=next[1]=NULL;
}
};
Trie *head;
bool ans;
void Insert(string str)
{
int i,j,k;
Trie *cur=head;
for(i=0;i<str.size();i++)//插入当前参数的str01串
{
int temp=str[i]-'0';
if(cur->next[temp]==NULL)//开辟新的空间给串的下一个字符,因为要开辟新空间以为着肯定不会有共同前缀了
{
cur->next[temp]=new Trie();
cur=cur->next[temp];
}
else//要是到第I字符都已存在的01串的某个字符一样,就需要判断已存在的01串是否完结,完结就即是有共同前缀了,就需要改变全局变量的值
{
cur=cur->next[temp];
if(cur->end)
ans=false;
}
}
cur->end=true;
}
void del(Trie *head)
{
for(int i=0;i<2;i++)
{
if(head->next[i]!=NULL)
del(head->next[i]);
}
delete head;
}
int main()
{
char str[15];
int Casenum=0;
head=new Trie();
ans=true;
while(gets(str))
{
if(str[0]!='9')
{
Insert(str);
}
else if(str[0]=='9')
{
Casenum++;
if(ans)
cout<<"Set "<<Casenum<<" is immediately decodable"<<endl;
else
cout<<"Set "<<Casenum<<" is not immediately decodable"<<endl;
del(head);//因为有多组数据输入,所以要清空前一组的数据,还要把头结点重新new一次,重新恢复全局变量的状态
head=new Trie();
ans=true;
}
}
}
例四:这个例题和例三很相似,虽然例三的代码能通过hdoj,AC了,但是事实上存在着缺憾的,在例四这道和例三大同小异的例题中就看到例三缺少了一个判断。
2 3 911 97625999 91125426 5 113 12340 123440 12345 98346
NO YES
题意:先输入有几个测试数据(2个),接着输入每组测试数据中有几个数字串(第一组3个,第二组5个),接着输入数字串,如果这些数字串有共同前缀就输出NO,否则输出YES。做法就是建立字典树,与例三相似,在数字串结束的结点标识个end=true,在插入更长的新的数字串的时候遇到这个end=true就代表你正在插入的新串与已存在的串具有共同前缀(前缀就是那个已存在的串本身),但是这里忽略了一种情况(也正是例三忽略的地方),先输入长串(123),那么在3的结点end=true,再输入短串(12),明明是有共同前缀12的,但是插入12的时候并不会碰到(123)的3:end=true,所以这种情况就漏了,如果例三输入010,01这样的话,结果就会出错了。
以下代码给出了解决的办法:
#include<iostream>
#include<cstdio>
#include<string>
using namespace std;
struct Trie
{
bool end;//数字串结束标志
Trie *next[10];//这里数字只有0-9,所以就10个大小的行了
Trie()
{
end=false;
for(int i=0;i<10;i++)
next[i]=NULL;
}
};
bool ans;
Trie *head;
void Insert(string num)
{
int i,j;
Trie *cur=head;
for(i=0;i<num.size();i++)
{
int temp=num[i]-'0';
if(cur->next[temp]==NULL)//如果之前没这个节点,那么肯定就不会有与这个串有共同前缀的串存在了,所以不用判断
{
cur->next[temp]=new Trie();
cur=cur->next[temp];
}
else
{
cur=cur->next[temp];
if(cur->end)//如果在插入新串的时候遇到之前的串的完结标志就表明有共同前缀了
ans=false;
}
}
cur->end=true;
for(i=0;i<10;i++)//新增加的判断,如果新插入的串完结了,在它后面还有字符,那么这个新串肯定是已存在的串的一个前缀了
{
if(cur->next[i]!=NULL)
ans=false;
}
}
void del(Trie *head)
{
int i;
for(i=0;i<10;i++)
{
if(head->next[i]!=NULL)
del(head->next[i]);
}
delete head;
}
int main()
{
int t,i,j,k,n;
string num;
cin>>t;
while(t--)
{
head=new Trie();//每来一个新测试数据组就应该把前一个组的字典树删除,创建一个新的字典树来保存新数据
ans=true;
cin>>n;
for(i=0;i<n;i++)
{
cin>>num;
Insert(num);
}
if(ans)
cout<<"YES"<<endl;
else
cout<<"NO"<<endl;
del(head);
}
}
例5:这是字典树最后一个例题了,写完这个下一个专题打算是树状数组。
好吧,这个例题是:HDU1247-Hat’s Words
a ahat hat hatword hziee word
ahat hatword
题意讲解:输入一些单词(按字母顺序输入的),输出那些由其它2个单词组成的单词(也是按字母顺序输出的),例如ahat就是由a,hat这2个输入的单词组成的所以输出,hatword同理。这题的做法是对于输入的每个单词全部细分成2个单词,去找这2个单词存不存在,举个例子,hatword(1)找h和atword存不存在(2)找ha和tword存不存在(3)找hat和word存不存在->都存在,输出hatword并break,找下一个单词。这样的做法很好理解,但是却需要耗费大量时间在查找2个单词存不存在上,因此用字典树来实现查找是最省时间的了。
#include<iostream>
#include<string>
#include<cstdio>
#include<vector>
using namespace std;
struct Trie
{
bool end;
Trie *next[26];
Trie()
{
end=false;
for(int i=0;i<26;i++)
{
next[i]=NULL;
}
}
};
Trie *head=new Trie();
void Insert(string word)
{
Trie *cur=head;
for(int i=0;i<word.size();i++)
{
int temp=word[i]-'a';
if(cur->next[temp]==NULL)
{
cur->next[temp]=new Trie();
cur=cur->next[temp];
}
else
{
cur=cur->next[temp];
}
}
cur->end=true;
}
//插入函数基本上与之前的例子大同小异,主要看下下面的查找函数
bool Find(string word)
{
Trie *cur=head;
for(int i=0;i<word.size();i++)//按要查找的单词一个字符一个字符地找
{
int temp=word[i]-'a';
if(cur->next[temp]==NULL)
return false;//没完结就断开了,肯定就不存在了
else
cur=cur->next[temp];
}
return cur->end;//完结了却没完结标志就返回没完结的标志false,否则完结了就返回完结的标志true
}
int main()
{
vector<string> dic;
char word[20];
int i,j;
while(scanf("%s",word)!=EOF)//这个换用gets()是AC不到的,注意下
{
dic.push_back(string(word));
Insert(word);
}
for(i=0;i<dic.size();i++)
{
for(j=1;j<dic[i].size();j++)
{
if(Find(dic[i].substr(0,j)) && Find(dic[i].substr(j)))
{
cout<<dic[i]<<endl;
break;
}
}
}
}
总结下吧:
看了5个例子,字典树的模板基本就出来了,主要处理的问题大概就是存储大量单词,查找单词是否存在(如例一事实是在找那个火星文是否存在,例5更是露骨的查找单词了),处理单词之间的前缀问题(如例2,3,4都是处理前缀的问题)。
模板:
结构体:
struct Trie
{
bool end;//作为一个串完结的标志,不同的题目不同,充分利用它能处理好多问题
Trie *next[26];//指向当前字母后一个字母的指针,若是字母就26,数字就10
Trie()
{
end=false;
for(int i=0;i<26;i++)
{
next[i]=NULL;
}
}
};
插入函数:
void Insert(string word)
{
Trie *cur=head;
for(int i=0;i<word.size();i++)//逐个字符插,插的过程中可以添加一些判断语句来处理不同的问题
{
int temp=word[i]-'a';
if(cur->next[temp]==NULL)
{
cur->next[temp]=new Trie();
cur=cur->next[temp];
}
else
{
cur=cur->next[temp];
}
}
cur->end=true;//插完之后当然要记得标上完结标签
}
查找函数:
bool Find(string word)
{
Trie *cur=head;
for(int i=0;i<word.size();i++)//也是逐个字母查找
{
int temp=word[i]-'a';
if(cur->next[temp]==NULL)//这里是串没完就断开
return false;
else
cur=cur->next[temp];
}
return cur->end;//找到串的长度,有2个可能,一个是那个串的确是存在的,另一种是那个串只是已存在的串的一个子串,未必存在,例如字典树只有apple这一个串,你查找的是app,的确能到底到app最后一个p的结点处,但是那个结点没完结标志就意味着这个app并非真实存在的
}