回说到,小Hi和小Ho接受到了河蟹先生伟大而光荣的任务:河蟹先生将要给与他们一篇从互联网上收集来的文章,和一本厚厚的河蟹词典,而他们要做的是判断这篇文章中是否存在那些属于河蟹词典中的词语。
当时,小Hi和小Ho的水平还是十分有限,他们只能够想到:“枚举每一个单词,然后枚举文章中可能的起始位置,然后进行匹配,看能否成功。”这样非常朴素的想法,但是这样的算法时间复杂度是相当高的,如果说词典的词语数量为N,每个词语长度为L,文章的长度为M,那么需要进行的计算次数是在N*M*L这个级别的,而这个数据在河蟹先生看来是不能够接受的。
于是河蟹先生决定先给他们个机会学习一下,于是给出了一个条件N=1,也就是说词典里面事实上只有一个词语,但是希望他们能够统计这个词语在文章中出现的次数,这便是我们常说的模式匹配问题。而小Hi和小Ho呢,通过这一周的努力,学习钻研了KMP算法,并在互相帮助之下,已经成功的解决掉了这个问题!
这便是Hiho一下第三周发生的事情,而现在第四周到了,小Hi和小Ho也要踏上解决真正难题的旅程了呢!
小Hi和小Ho是一对好朋友,出生在信息化社会的他们对编程产生了莫大的兴趣,他们约定好互相帮助,在编程的学习道路上一同前进。
这一天,他们……咳咳,说远了,且说小Ho好不容易写完了第三周程序,却发现自己错过了HihoCoder上的提交日期,于是找小Hi哭诉,小Hi虽然身为管理员,但是也不好破这个例,于是把小Ho赶去题库交了代码,总算是哄好了小Ho。
小Ho交完程序然后屁颠屁颠的跑回了小Hi这边,问道:“小Hi,你说我们是不是可以去完成河蟹大大的任务了呢?”
小Hi思索半天,道:“老夫夜观星象……啊不,我这两天查阅了很多资料,发现这个问题其实也是很经典的问题,早在06年就有信息学奥林匹克竞赛国家集训队的论文中详详细细的分析了这一问题,而他们使用的就是Trie图这样一种数据结构!”
“Trie图?是不是和我们在第二周遇到的那个Trie树有些相似呀?”小Ho问道。
“没错!Trie图就是在Trie树的基础上发展成的一种数据结构。如果要想用一本词典构成Trie图的话,那么就首先要用这本词典构成一棵Trie树,然后在Trie树的基础上添加一些边,就能够变成Trie图了!”小Hi又作老师状。
“哦!但是你说了这么多,我都不知道Trie图是什么样的呢!”小Ho无奈道。
“也是!那我们还是从头开始,先讲讲怎么用Trie树来解决这个问题,然后在Trie树的基础上,讨论下一步应该如何。”小Hi想了想说道。
提示一:如何用Trie树进行“河蟹”
“现在我们有了一个时间复杂度在O(ML)级别的方法,但是我们的征途在星辰大海,啊不,我们不能满足于这样一个60分的方法。所以呢,我们还是要贯彻我们一贯的做法,寻找在这个算法中那些冗余的计算!“小Hi道:”那么我们现在来看看Trie树进行计算的时候都发生了些什么。”
提示二:Trie树的优化思路——后缀结点
“那么现在……”小Hi刚要开口,就被小Ho无情打断。
“可是小Hi老师~你看在这种情况下,结点C找不到对应的后缀结点,它对应的路径是aaabc,而aabc在Trie里面是走不出来的!”小Ho手中挥舞着一张纸,问道。
“你个瓜娃子,老是拆老子台做啥子!……阿不,小Ho你别担心,我这就要讲解如何求后缀结点呢~”小Hi笑容满面的说道。
提示三:如何求解Trie树中每个结点的后缀结点
“原来如此!这样我就知道了每一个结点的后缀结点了,接下来我就可以很轻松的解决河蟹先生交给我的问题了呢!”小Ho高兴的说道:“但是,说好的Trie图在哪里呢?”
小Hi不由笑道:“你这叫买椟还珠你知道么?还记得我们再计算后缀结点的时候计算出的从每个点出发,经由每一个char(比如'a'..'d')会走到的结点么?把这些边添加到Trie树上,就是Trie图了!”
“原来是这样,但是这些边感觉除了计算后缀结点之外,没有什么用处呀?”小Ho又开始问问题了。
“这就是Trie图的巧妙之处了,你想想你什么时候需要知道一个结点的后缀结点?”小Hi实在不忍看自己的兄弟这般呆萌,只能耐着性子解释。
小Ho顿时恍然大悟,“在这个结点不能够继续和文章str继续匹配了的时候,也就是这个结点没有“文章的下一个字符”对应的那条边,哦!我知道了,在Trie图中,每个结点都补全了所有的边,所以原来需要先找到后缀结点再根据“str的下一个字符”这样一条边找到下一个结点,现在可以直接通过当前结点的“str的下一个字符”这样一条边就可以接着往下匹配了,如果本来是有这条边的,那不用多说,而如果这条边是根据后缀结点补全的,那便是我们想要的结果!”
“所以呢!完成这个任务的方法总的来说就是这样,先根据字典构建一棵Trie树,然后根据我们之前所说的构建出对应的Trie图,然后从Trie图的根节点开始,沿着文章str的每一个字符,走出对应的边,直到遇到一个标记结点或者整个str都已经匹配完成了~”小Hi适时的总结道。
“而这样的时间复杂度则在O(NL+M)级别的呢!想来是足以完成河蟹先生的要求了呢~”小Ho搬了搬手指,说道。
“是的!但是河蟹先生要求的可不是想法哦,他可是希望我们写出程序给它呢!”
每个输入文件有且仅有一组测试数据。
每个测试数据的第一行为一个整数N,表示河蟹词典的大小。
接下来的N行,每一行为一个由小写英文字母组成的河蟹词语。
接下来的一行,为一篇长度不超过M,由小写英文字母组成的文章。
对于60%的数据,所有河蟹词语的长度总和小于10, M<=10
对于80%的数据,所有河蟹词语的长度总和小于10^3, M<=10^3
对于100%的数据,所有河蟹词语的长度总和小于10^6, M<=10^6, N<=1000
对于每组测试数据,输出一行"YES"或者"NO",表示文章中是否含有河蟹词语。
6 aaabc aaac abcc ac bcd cd aaaaaaaaaaabaaadaaac
#include<iostream> #include<string> #include<queue> using namespace std; #define MAX 26 //int ct=0; struct trie_node//node { // int data; bool fined; // int depth;//深度 bool dangerous;//判断是否危险 char bian;//相连边 trie_node *child[MAX];//该节点连接的孩子 trie_node *pre;//后缀结点 trie_node *father;//父亲结点 }; void trie_node_insert(trie_node *root,string s)//insert a new string { if(root==NULL||s.size()==0) return; int i=0; trie_node *p=root; while(s[i]!='\0'&&p->dangerous==0) { if(s[i]>='a'&&s[i]<='z') { if(p->child[s[i]-'a']==NULL)//if do not exit,build a new node; { // ct++; /* build a new node */ trie_node *temp=new trie_node; temp->dangerous=0; // temp->data=ct; // temp->depth=i+1; temp->bian=s[i]-'a'; temp->father=p; temp->fined=0; if(i==0) temp->pre=root; else temp->pre=NULL; for(int j=0;j<MAX;j++) temp->child[j]=NULL; p->child[s[i]-'a']=temp;//change p } p=p->child[s[i]-'a']; } if(i==s.size()-1) p->dangerous=1; i++; } } /* 找后缀的目的在于设置新的危险点,设置新的连线 */ void find_pre(trie_node *p)//找后缀 { p->fined=1; if(p->pre==NULL)//如果后缀不是根节点 { trie_node *q=p->father->pre; while(q->child[p->bian]==NULL) { if(q->father==q) { p->pre=q;//后缀是根节点 // cout<<p->data<<" "<<p->pre->data<<endl; for(int i=0;i<MAX;i++) if(p->child[i]==NULL) p->child[i]=q->child[i]; break; } q=q->pre; } if(q->child[p->bian]!=NULL) { p->pre=q->child[p->bian];//后缀不是根节点 // cout<<p->data<<" "<<p->pre->data<<endl; if(p->pre->dangerous==0)//如果后缀结点不危险 { for(int i=0;i<MAX;i++) if(p->child[i]==NULL&&p->pre->child[i]!=NULL) p->child[i]=p->pre->child[i]; } else//如果后缀结点危险 { // cout<<p->pre->data<<" "<<p->pre->dangerous<<endl; p->dangerous=1; for(int i=0;i<MAX;i++) { if(p->child[i]!=NULL) { if(p->child[i]!=p)//如果第i个孩子不是自己就把那个孩子删掉节省空间 delete p->child[i]; p->child[i]=NULL; } } } } }else //对于第0层第1层的结点 { if(p->dangerous==0)//如果不是危险结点则扩展,如果是危险结点那么所有孩子结点都没有现在也应该没有 for(int i=0;i<MAX;i++) if(p->child[i]==NULL&&p->pre->child[i]!=NULL) p->child[i]=p->pre->child[i]; } } void trie2(trie_node *root)//将trie树转化为trie图 { queue<trie_node*> d; trie_node *p=root; // cout<<p->data<<" "<<p->depth; d.push(p); while(d.size()>0&&p!=NULL) { p=d.front(); p->fined=1; for(int i=0;i<MAX;i++) { if(p->child[i]!=NULL&&p->child[i]->fined==0) { // if(p->child[i]->pre==NULL) find_pre(p->child[i]); /* if(p->child[i]->pre->dangerous==1) { for(int k=0;k<MAX;k++) { p->child[i]->child[k]=NULL; } } */ // cout<<p->child[i]->data<<" "<<p->child[i]->bian<<endl; d.push(p->child[i]); } } d.pop(); } } void search(trie_node *root,string s) { trie_node *p=root; int i=0; while(i<s.size()) { // cout<<s[i]<<endl; if(p->child[s[i]-'a']!=NULL) { p=p->child[s[i]-'a']; if(p->dangerous==1) { cout<<"YES"<<endl; return; } }else p=root; i++; } if(i==s.size()) cout<<"NO"<<endl; else cout<<"YES"<<endl; } int main(void) { trie_node *root=new trie_node; root->dangerous=0; // root->data=ct; root->pre=root; root->fined=0; // root->depth=0; root->bian=-1; root->father=root; for(int i=0;i<MAX;i++) root->child[i]=NULL; int n; cin>>n; while(n--) { string temp; cin>>temp; trie_node_insert(root,temp); } trie2(root); string str; // cout<<"input"<<endl; cin>>str; search(root,str); return 0; }