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

TrieTrie上建立“前綴邊”,不用再像在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自動機

剛開始直接用沒有修改的build函數和query函數,導致query每次還得查詢當前詞的後綴,引起TLE後來發現如果其後綴是河蟹詞,將其標記爲危險可以避免查詢當前詞的後綴
#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;  
}  

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章