콘텐츠로 이동

Aho Corasick

Problem

Definition 1

문자열 사전 \(TV\)의 Trie를 만든 후, 각 노드에 대하여 \(par[v]\), \(fail[v]\), \(suf[v]\)를 정의한다.

  • \(par[v]:=\) Trie에서 노드 \(v\)의 부모
  • \(fail[v]:=\) \(v\)의 노드가 의미하는 prefix의 proper suffix 중, Trie에 존재하는 prefix와 동일한 것 중 최대 길이의 prefix로의 링크 (failure link)
  • \(suf[v]:=\) 노드 \(v\)에서 failure link를 타고 올라가면서 만나는 첫 번째 terminal node로의 링크 (suffix link)

Trie의 루트 노드와 루트 노드의 자식 노드들의 fail 링크는 루트 노드로 정의한다. \(par[root]=fail[root]=suf[root]=root\)로 정의한다.

문자열 \(S\)와 문자열 사전 \(TV\)가 주어졌을 때, \(TV\)의 trie를 만들고 이를 이용하여 \(S\)에서 \(TV\)의 모든 등장 위치를 탐색한다.

image 1

“a”, “ab”, “bc”, “bca”, “c”, “caa”가 문자열 \(TV[0], TV[1], \cdots\)일 때 Trie를 나타낸 그림이다.
파란색 간선이 failure Link, 초록색 간선이 suffix Link를 의미한다.

Algorithm

문자열 \(TV[0], TV[1], \cdots\) 들을 하나씩 Trie에 삽입한다. 이후, BFS를 하며 루트에서 가까운 노드부터 보며 \(fail[v]\)를 결정한다. \(fail[v]\)를 구하기 위하여, \(fail[par[v]], fail[fail[par[v]]], \cdots\) 의 자식 간선에 \(v\)가 의미하는 prefix의 마지막 문자 \(c\)가 존재하는지 확인하고, 자식 문자 \(c\)가 존재하는 첫 번째 노드의 \(c\)에 해당하는 자식 노드를 \(fail[v]\)로 결정한다.

완성된 Trie와 Suffix Link (Failure Link)를 바탕으로, 비교하려고 하는 문자열 \(S\)의 각 문자를 하나씩 보며 탐색한다. 시작 노드는 루트에서 시작하며, \(S\)의 문자를 하나씩 보며, 현재 노드의 자식 문자 간선이 존재하면, 그 노드로 이동하고, 존재하지 않으면, 존재할 때까지 Suffix Link를 타고 이동하며 비교를 반복한다.

Complexity

\(N = |S|\), \(M = \sum |TV[i]|\), \(\sum\) = 알파벳 개수
Time Complexity : \(O(N+M)\)
Space Complexity : \(O(M\sum)\)

Code

aho_corasick.cpp
namespace AhoCorasick
{
    // par[v] = parent of node v
    // fail[v] = failure link of node v
    // suf[v] = suffix link of node v
    // chd[v] = children of node v in trie
    struct Node
    {
        int par, fail, suf;
        vector<int> chd;
        Node()
        {
            par=fail=suf=-1;
            chd=vector<int>(26, -1);
        }
    };

    int root;
    vector<Node> NS;

    // Maximum size of NS is |SV[0]|+|SV[1]|+...
    int newNode()
    {
        NS.push_back(Node());
        return NS.size()-1;
    }

    // Make trie of dictionary SV, and determine par[v], fail[v], suf[v] for all nodes
    // SV[0], SV[1], ... is 0-based
    void makeTrie(vector<string> SV)
    {
        root=newNode();
        NS[root].par=root; NS[root].fail=root; NS[root].suf=root;

        for(auto &S : SV)
        {
            int now=root;
            for(auto c : S)
            {
                if(NS[now].chd[c-'a']==-1)
                {
                    int nxt=NS[now].chd[c-'a']=newNode();
                    NS[nxt].par=now;
                }
                now=NS[now].chd[c-'a'];
            }
            NS[now].suf=now;
        }

        queue<int> Q;
        Q.push(root);
        while(!Q.empty())
        {
            int now=Q.front(); Q.pop();
            for(int i=0; i<26; i++) if(NS[now].chd[i]!=-1)
            {
                int nxt=NS[now].chd[i];
                if(now==root) NS[nxt].fail=root;
                else
                {
                    int p;
                    for(p=NS[now].fail; p!=root; p=NS[p].fail) if(NS[p].chd[i]!=-1) break;
                    if(NS[p].chd[i]==-1) NS[nxt].fail=root;
                    else NS[nxt].fail=NS[p].chd[i];
                }

                if(NS[nxt].suf==-1) NS[nxt].suf=NS[NS[nxt].fail].suf;
                Q.push(nxt);
            }
        }
    }

    // Find occurences of TV[0], TV[1], ... in S (ending position)
    // S, TV[0], TV[1], ... is 0-based
    // AhoCorasick(S = "mississippi", TV = ["ss", "sis", "ippi", "pp"]) = [3, 5, 6, 9, 10]
    vector<int> AhoCorasick(string S, vector<string> TV)
    {
        vector<int> ans;
        makeTrie(TV);

        int now=root;
        for(int i=0; i<S.size(); i++)
        {
            for(; now!=root && NS[now].chd[S[i]-'a']==-1; now=NS[now].fail);
            if(NS[now].chd[S[i]-'a']!=-1) now=NS[now].chd[S[i]-'a'];

            // now is matching node in trie with S[0...i]
            // your code goes here

            if(NS[now].suf!=root) ans.push_back(i);
        }
        return ans;
    }
}

Details

template
namespace AhoCorasick
{
    // par[v] = parent of node v
    // fail[v] = failure link of node v
    // suf[v] = suffix link of node v
    // chd[v] = children of node v in trie
    struct Node
    {
        int par, fail, suf;
        vector<int> chd;
    };

    int root;
    vector<Node> NS;
    int newNode() {}

    // Make trie of dictionary SV, and determine par[v], fail[v], suf[v] for all nodes
    // SV[0], SV[1], ... is 0-based
    void makeTrie(vector<string> SV) {}

    // Find occurences of TV[0], TV[1], ... in S (ending position)
    // S, TV[0], TV[1], ... is 0-based
    vector<int> AhoCorasick(string S, vector<string> TV) {}
}
  • void makeTrie(vector<string> SV) : 문자열 사전 \(SV\)의 Trie를 만들고, 모든 노드의 \(par[v]\), \(fail[v]\), \(suf[v]\)를 결정
  • vector<int> AhoCorasick(string S, vector<string> TV) : \(S\)에서 문자열 사전 \(TV\)의 등장 위치(끝 인덱스)를 리턴함
    • \(S, TV[0], TV[1], \cdots\)는 0-based
example
1
2
3
4
5
6
7
void test_aho_corasick()
{
    vector<int> V;

    V = AhoCorasick::AhoCorasick("mississippi", {"ss", "sis", "ippi", "pp"});
    assert(V == vector<int>({3, 5, 6, 9, 10}));
}