一、先简单的介绍一下字典树(一下关于字典树的介绍的内容来自互联网)
trie的性质真的是相当的好,而且实现比较简单。它使在字符串集合中查找某个字符串的操作的复杂度降到最大只需O(n),其中n为字符串的长度。trie是典型的将时间置换为空间的算法,好在ACM中一般对空间的要求很宽松。
trie的原理是利用字符串集合中字符串的公共前缀来降低时间开销以达到提高效率的目的。它具有以下性质:1,根结点不包含任何字符信息;2,如果字符的种数为n,则每个结点的出度为n(这样必然会导致浪费很多空间,这也是trie的缺点,我还没有想到好点的办法避免);3,查找,插入复杂度为O(n),n为字符串长度。
举一个例子,给50000个由小写字母构成的长度不超过10的单词,然后问某个公共前缀是否出现过。如果我们直接从字符串集中从头往后搜,看给定的字符串是否为字符串集中某个字符串的前缀,那样复杂度为O(50000^2),这样显然会TLE。又或是我们对于字符串集中的每个字符串,我们用MAP存下它所有的前缀。然后询问时可以直接给出结果。这样复杂度为O(50000*len),最坏情况下len为字符串最长字符串的长度。而且这没有算建立MAP存储的时间,也没有算用MAP查询的时间,实际效率会更低。但如果我们用trie的话,当查询如字符串abcd是否为某字符串的前缀时,显然以b,c,d....等不是以a开头的字符串就不用查找了。实际查询复杂度只有O(len),建立trie的复杂度为O(50000).这是完全可以接受的。
如给定字符串集合abcd,abd,cdd,efg,hij,hi六个字符串建立的trie tree如下图所示:
查找一个字符串时,我们只需从根结点按字符串中字符出现顺序依次往下走。如果到最后字符串结束时,对应的结点标记为红色,则该字符串存在;否则不存在。
插入时也只需从根结点往下遍历,碰到已存在的字符结点就往下遍历,否则,建立新结点;最后标记最后一个字符的结点为红色即可。
二、解题思路分析 题目描述在http://acm.pku.edu.cn/JudgeOnline/problem?id=1451
第一步:要根据输入的单词,建一颗字典树,建树的时候需要注意的是:想字典树种插入单词的时候需要将单词的出现的可能性(就是输入时每个单词后的那个整数)累加到组成这个单词的路径上的每个结点上,因为题目说每个字符串出现的可能性是所有以该字符串为前缀的所有单词的出现可能性的和。这样建好字典树后,就可以查找任意字符串出现的可能性。
第二步:根据输入的键盘数字序列进行深度搜索,并进行剪枝(当某个个字符串出现的可能性<0时,那么字串长度>该字符串长度的字符串,就不用搜索了,直接剪掉)。
//poj1451 T9英文输入法
//解题思路:字典树+深度搜索
//将单词存入到字典树中,并且每个结点还要存放到达当前结点的串的概率
//深度搜索所有可能的组合,在字典树中去找到该组合出现的概率,将概率最大的字符串的组合打印出来
三、题目源码(今天第一次用字典树,更没想一次就AC了,鼓掌。。。)
#include<iostream>
#include<vector>
using namespace std;
/*字典树的数据结构*/ //本字典树只可用于小写字母
struct trie
{
trie * next[26];
int pro; // pro 代表 probability表示概率
};
trie thead,*t,*s;
trie DTree;
int w;
char word[20];
int p;
int keyNum[102];
char ans[102];
char best_ans[102][20];
int max_p[102];
int length;
char NumToChar[10][5]={"00","00","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};//键盘数字与字符的对应
int search(trie &head,char x[])//在数中查找(一个字符串x)返回他出现的概率 。如果返回0便是查找失败
{
int i;
int len;
len=strlen(x);
if(len==0) return 0;
s=&head;
for(i=0;i<len;i++) //取单词x中的每个字符去检查是否等于存在于树中
{
if(s->next[x[i]-'a']==NULL) break;// 如过这里为空,说明这里没有存字符x[i],即查找失败
else s=s->next[x[i]-'a']; //如果s->next[x[i]-'a']!=NULL,说明这里存在字符x[i] ,然后继续查找下一个字符
}
if(i==len) return (*s).pro;
else return 0;
}
int insert(trie &head,char x[],int proba) //插入一个单词x,并传入他出现的概率
{
int i,j;
int len;
len=strlen(x);
s=&head;
for(i=0;i<len;i++)
{
if(s->next[x[i]-'a']==NULL) break;
else
{
s=s->next[x[i]-'a'];
s->pro += proba; //概率增加
}
}
for(;i<len;i++)//当没有找到可生成这个单词x的路径时,就得分配空间,存储单词的各个字符
{
t=new trie();
for(j=0;j<26;j++) t->next[j]=NULL;
t->pro=proba;
s->next[x[i]-'a']=t;
s=t;
}
return 0;
}
void inittrie(trie &head) //初始化化一颗字典树
{
int i;
for(i=0;i<26;i++) //
head.next[i]=NULL;
head.pro=0;
}
void GetAnsWerDFS(int i)
{
if(keyNum[i]!=1 )
{
int k=3;
if( keyNum[i]==7 ||keyNum[i]==9)//如果当前的数字键对应着4个字母
k=4;
for(int j=0;j<k;j++)
{
ans[i]=NumToChar[keyNum[i]][j];
ans[i+1]='\0';
int cur_p=search(DTree,ans);
if(cur_p>max_p[i])
{
strcpy(best_ans[i],ans);
max_p[i]=cur_p;
}
if(cur_p>0) //剪枝,如果当前长度为i的字符串出现可能性>0,才需要去递归搜索长度为i+1的字符串
GetAnsWerDFS(i+1);
}
}
}
void Test(int T)
{
cout<<"Scenario #"<<T<<":"<<endl;
inittrie(DTree);//初始化字典树
cin>>w;
for(int i=0;i<w;i++)
{
cin>>word>>p;
insert(DTree,word,p); //将单词插入到字典树中
}
测试
/* char at[4][20]={"id","love","next","hell"};
for(int i=0;i<4;i++)
cout<<search(DTree,at[i])<<endl;*/
///
int t=0;// 输入的按键数字序列个数为t
cin>>t;
for(int i=0;i<t;i++)
{
memset(keyNum,0,sizeof(keyNum));
memset(ans,0,sizeof(ans));
memset(best_ans,0,sizeof(best_ans));
memset(max_p,0,sizeof(max_p));
length=0;
char tempkeyNum[20];
cin>>tempkeyNum;
while(true)
{
keyNum[length]=tempkeyNum[length]-'0';
if(keyNum[length]==1)break;
length++;
}
GetAnsWerDFS(0);
for(int k=0;k<length;k++)
{
if(strlen(best_ans[k])!=0)
cout<<best_ans[k]<<endl;
else cout<<"MANUALLY"<<endl;
}
cout<<endl;
}
cout<<endl;
}
int main()
{
int N=0;
cin>>N;
for(int T=1;T<=N;T++)
Test(T);
return 0;
}