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
Trie圖是Trie樹上建立“前綴邊”,不用再像在Trie樹上那樣順着fail一個一個往上跳了,省了不少時間。這種做法在hihoCoder 上時間排到了前三名。
#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;
}
不要介意“前綴邊”這個名字起得多麼牽強,可以理解爲記錄fail最終跳到的點,直接指過去就行了。gty學長講課時也講過這種優化。法一: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;
}