HiHocoder 1036 : Trie图 AC自动机
Trie图
先看一个问题:给一个很长很长的母串 长度为n,然后给m个小的模式串。求这m个模式串里边有多少个是母串的字串。
最先想到的是暴力O(n*m*len(m)) len(m)表示这m个模式串的平均长度。。。
显然时间复杂度会很高。。。
再改进一些,用kmp让每一模式串与母串进行匹配呢?时间复杂度为O((n + len(m))*m),还算可以。
可是还有没有更快的算法呢?
编译原理里边有一个很著名的思想:自动机。
这里就要用到确定性有限状态自动机(DFA)。可以对这m个模式串建立一个DFA,然后让母串在DFA上跑,遇到某个模式串的终结节点则表示这个模式串在母串上。
就像这个图,母串“nano”在上边跑就能到达终止节点。
上边说的是自动机的概念。。。还有一个要用到的是trie树,这个不解释了,网上资料一大堆。
这里步入正题:Trie图
trie图是一种DFA,可以由trie树为基础构造出来,
对于插入的每个模式串,其插入过程中使用的最后一个节点都作为DFA的一个终止节点。
如果要求一个母串包含哪些模式串,以用母串作为DFA的输入,在DFA 上行走,走到终止节点,就意味着匹配了相应的模式串。
ps: AC自动机是Trie的一种实现,也就是说AC自动机是构造Trie图的DFA的一种方法。还有别的构造DFA的方法...
怎么建Trie图?
可以回想一下,在kmp算法中是如何避免母串在匹配过程种指针回溯的?也就是说指针做不必要的前移,浪费时间。
同样的,在trie图中也定义这样一个概念:前缀指针。
这个前缀指针,从根节点沿边到节点p我们可以得到一个字符串S,节点p的前缀指针定义为:指向树中出现过的S的最长的后缀。
构造前缀指针的步骤为:根据深度一一求出每一个节点的前缀指针。对于当前节点,设他的父节点与他的边上的字符为Ch,如果他的父节点的前缀指针所指向的节点的儿子中,有通过Ch字符指向的儿子,那么当前节点的前缀指针指向该儿子节点,否则通过当前节点的父节点的前缀指针所指向点的前缀指针,继续向上查找,直到到达根节点为止。
上图构造出所有节点的前缀指针。
相信原来的问题到这里基本已经解决了。可以再考虑一下它的时间复杂度,设M个串的总长度为LEN
所以算法总的时间复杂度为O(LEN + n)。比较好的效率。
模板,HDU 2222:
/* 个人感觉这样写更清晰一点。(动态分配内存) */ class Node { public: Node* fail; Node* next[26]; int cnt; Node() { CL(next, 0); fail = NULL; cnt = 0; } }; //Node* q[10000000]; class AC_automaton : public Node{ public: Node *root; int head, tail; void init() { root = new Node(); head = tail = 0; } void insert(char* st) { Node* p = root; while(*st) { if(p->next[*st-'a'] == NULL) { p->next[*st-'a'] = new Node(); } p = p->next[*st-'a']; st++; } p->cnt++; } void build() { root->fail = NULL; deque<Node* > q; q.push_back(root); while(!q.empty()) { Node* tmp = q.front(); Node* p = NULL; q.pop_front(); for(int i = 0; i < 26; ++i) { if(tmp->next[i] != NULL) { if(tmp == root) tmp->next[i]->fail = root; else { p = tmp->fail; while(p != NULL) { if(p->next[i] != NULL) { tmp->next[i]->fail = p->next[i]; break; } p = p->fail; } if(p == NULL) tmp->next[i]->fail = root; } q.push_back(tmp->next[i]); } } } } int search(char* st) { int cnt = 0, t; Node* p = root; while(*st) { t = *st - 'a'; while(p->next[t] == NULL && p != root) { p = p->fail; } p = p->next[t]; if(p == NULL) p = root; Node* tmp = p; while(tmp != root && tmp->cnt != -1) { cnt += tmp->cnt; tmp->cnt = -1; tmp = tmp->fail; } st++; } return cnt; } }AC;以上转载自:http://www.cnblogs.com/vongang/archive/2012/07/24/2606494.html
Trie图:http://hihocoder.com/problemset/problem/1036
#include<cstdio> #include<cstring> #include<algorithm> #define N 1000006 using namespace std; int c[N][26], cnt = 0, fail[N], n, q[N], w[N]; inline void ins(char *s) { int len = strlen(s), now = 0; for(int i = 0; i < len; ++i) { int t = s[i] - 'a'; if (!c[now][t]) c[now][t] = ++cnt; now = c[now][t]; } w[now] = 1; } inline void BFS() { int now, head = -1, tail = -1; for(int t = 0; t < 26; ++t) if (c[0][t]) q[++tail] = c[0][t]; while (head != tail) { now = q[++head]; for(int t = 0; t < 26; ++t) if (!c[now][t]) c[now][t] = c[fail[now]][t]; //建立“前缀边” else { q[++tail] = c[now][t]; int tmp = fail[now]; while(tmp && !c[tmp][t]) tmp = fail[tmp]; fail[c[now][t]] = c[tmp][t]; } } } inline void AC(char *s) { int len = strlen(s), now = 0; for(int i = 0; i < len; ++i) { now = c[now][s[i] - 'a']; if (w[now]) { puts("YES"); return; } } puts("NO"); } int main() { scanf("%d\n", &n); char s[N]; for(int i = 1; i <= n; ++i) scanf("%s", s), ins(s); BFS(); scanf("%s", s); AC(s); return 0; }不要介意“前缀边”这个名字起得多么牵强,可以理解为记录
法一:Trie图
讲的很详细,又是已经会了手动操作,变成代码还是有点困难,按照郭老师那个模版敲了一个差不多的,但是感觉和本题所讲写的不一样,让我再研究一下
#include <cstdio> #include <cstring> #include <queue> using namespace std; int n; char s[1000005]; struct Node { bool isend; Node *nxt[26],*pre; Node():isend(false),pre(NULL) { memset(nxt,NULL,sizeof(nxt)); } }*root,*cur,*pre; void add(char *p) {//添加模式串,建立trie树 cur=root; while(*p) { if(cur->nxt[*p-'a']==NULL) cur->nxt[*p-'a']=new Node(); cur=cur->nxt[*p-'a']; ++p; } cur->isend=true; } void build() {//建立trie图 cur=root; queue<Node*> q; for(int i=0;i<26;++i) if(root->nxt[i]) {//第一层结点的前缀指针指向根结点 cur->nxt[i]->pre=root; q.push(cur->nxt[i]); } while(!q.empty()) { cur=q.front(); q.pop(); for(int i=0;i<26;++i) { if(cur->nxt[i]) {//如果当前结点存在i子结点 pre=cur->pre; while(pre) { if(pre->nxt[i]) {//找到当前结点的有i子结点的前缀结点 cur->nxt[i]->pre=pre->nxt[i]; if(pre->nxt[i]->isend)//如果该前缀结点危险结点,则其i子结点也是危险结点 cur->nxt[i]->isend=true; break; } pre=pre->pre; } if(cur->nxt[i]->pre==NULL)//如果未找到当前结点的有i子结点的前缀结点,则其i子结点的前缀结点是根节点 cur->nxt[i]->pre=root; q.push(cur->nxt[i]); } } } } bool query(char *p) { int i; cur=root; while(*p) { i=*p-'a'; while(cur) { if(cur->nxt[i]) { cur=cur->nxt[i]; if(cur->isend==true) return true; break; } cur=cur->pre; } if(cur==NULL)//若trie图中没有以*p开头的模式串,当前结点指向根结点 cur=root; ++p; } return false; } int main() { root=new Node(); scanf("%d",&n); while(n--) { scanf("%s",s); add(s); } build(); scanf("%s",s); printf("%s\n",query(s)?"YES":"NO"); return 0; }
法二:AC自动机
#include <cstdio> #include <queue> using namespace std; const int MAXNODE=1000005; struct Trie { int nxt[MAXNODE][26],fail[MAXNODE]; bool ed[MAXNODE]; int l; const static int root=0; Trie() { clear(); } int newNode() { for(int i=0;i<26;++i) nxt[l][i]=-1; ed[l]=false; return l++; } void insert(char *p) { int cur=root; while(*p) { if(nxt[cur][*p-'a']==-1) nxt[cur][*p-'a']=newNode(); cur=nxt[cur][*p-'a']; ++p; } ed[cur]=true; } void build() { int cur=root,i; queue<int> q; fail[root]=root; for(i=0;i<26;++i) { if(nxt[root][i]==-1) nxt[root][i]=root; else { fail[nxt[root][i]]=root; q.push(nxt[root][i]); } } while(!q.empty()) { cur=q.front(); q.pop(); for(i=0;i<26;++i) { if(nxt[cur][i]==-1) nxt[cur][i]=nxt[fail[cur]][i]; else { fail[nxt[cur][i]]=nxt[fail[cur]][i]; q.push(nxt[cur][i]); if(ed[fail[nxt[cur][i]]])//优化,与普通的AC自动机不同,因为只要有河蟹词就返回,所以有河蟹词后缀的也标记危险,去掉查询时通过while查询后缀 ed[nxt[cur][i]]=true; } } } } bool query(char *p) { int cur=root; while(*p) { cur=nxt[cur][*p-'a']; if(ed[cur]) return true; ++p; } return false; } void clear() { l=root; newNode(); } }ac; int n; char s[MAXNODE]; int main() { scanf("%d",&n); while(n--) { scanf("%s",s); ac.insert(s); } ac.build(); scanf("%s",s); printf("%s\n",ac.query(s)?"YES":"NO"); return 0; }