字串相關 Aho Corasick 自動機

2022-09-14 19:09:16 字數 3763 閱讀 3127

下面提到的「前置知識」只是如果會的話更方便理解,但並非必須會。

詳見 自動機 - oi wiki。當然不做了解也沒有任何學習 \(\texttt\) 自動機的困難。

但是有一點還是需要了解:自動機上的每乙個節點代表的是乙個狀態,在 \(\tt\) 自動機上,代表的就是從根節點一路走到該節點所經過的邊上的字元所組成的字串。

\(\tt\) 自動機中用到了 \(\tt\) 匹配演算法 中的部分思想,(當然也相信各位一定都能熟練掌握 kmp),但如果沒有學習 \(\texttt\) 演算法的話依舊沒有任何問題。

對於單模式串的匹配我們可以採用 \(\texttt\) 演算法進行匹配,但是如果有多個模式串呢?

\(\texttt\) 自動機(\(\texttt\))就是一種多模式匹配演算法。它可以將多個模式串統一處理,對於每乙個待匹配的文字串都可以一次與多個模式串進行匹配。

考慮我們如何將多個不同的文字串聯絡起來,\(\tt\) 樹是乙個很常用的方法,而 \(\tt\) 自動機便是以 \(\tt\) 樹為骨架建造的。也就是將所有的模式串先插入到一棵 \(\tt\) 樹中,再結合上我們在 \(\tt\) 匹配演算法中的思想——假設與當前串不能匹配,那麼我們沿著失配邊走到當前串的乙個字尾末,看看可不可以繼續匹配,這種「失配邊」有乙個更加高階的名字——字尾鏈結,「字尾鏈結」的思想在許多字串演算法中都有使用。

我們在上面提到過了,\(\tt\) 樹就是 \(\tt\) 自動機的「骨架」,那麼在進行血肉的構建前,我們必須先把 \(\tt\) 樹建立起來。

\(\tt\) 自動機中 \(\tt\) 樹的建立過程與一般的 \(\tt\) 樹無異。

struct trie ;

inline void insert(char *s)

++ t[now].tag;

}

我們所需要做的不過是將所有的模式串都插入到這一棵 \(\tt\) 樹中。

如果說 \(\tt\) 樹是 \(\tt\) 自動機的骨架,那麼失配邊(字尾鏈結)便是令 \(\tt\) 自動機運作起來的強有力的肌肉,下面來重點看失配邊的作用與形成。

如果你學習過 \(\tt\) 匹配演算法便不難理解這裡的失配邊fail指標,它就類似於 \(\tt\) 演算法中的next陣列。每個節點的fail指標指向的是當前節點代表的字串的,在 \(\tt\) 樹上以字首出現的,最長的字尾所對應的節點

上面的整句話似乎有些繞或難以理解,來看下面這個例子:

我們在 \(\tt\) 樹中依次插入 \(\text\) 這三個單詞,得到的 \(\tt\) 樹如下:

根據上面並不清晰的定義,\(5\) 號節點的fail指標應當指向 \(2\) 號節點,因為 '\(\text\)' 是 '\(\text\)' 的乙個字尾,存在從 \(0\) 號點出發的乙個狀態 '\(\text\)',且是符合上述要求的最長的乙個。

要注意,如果不存在這樣地字尾,那麼該節點的fail應當指向 \(0\) 節點。

上圖完整地建出fail指標的樣子如下圖:

那麼,我們在匹配的時候,在 \(\tt\) 樹上逐步推進,如果發現當前節點不存在後面的字元對應的邊,那麼就沿著fail指標向前跳,直到能夠找到對應的邊或來到了 \(0\) 號節點(無法匹配該字元),那麼便可以繼續匹配下乙個字元。

那麼現在我們可以寫出乙個簡單的匹配函式:

/*先不要著急!這不是最終版本的查詢函式*/

inline void query(char *s) }}

下文中,會不嚴謹地稱呼「字尾鏈結指向的節點」為「字尾節點」。

那麼如何建立字尾鏈結呢?

首先先點出幾個很顯然的性質:

那麼,結合上面的性質,考慮如何運用已經求得的fail指標來算出當前節點的fail指標。我們可以想到通過 \(\tt\) 來建立整張圖的指標,大體流程如下:

當前要求 \(u\) 的fail指標,\(f\) 是 \(u\) 的父節點,\(f\) 通過 \(c\) 連向 \(u\):

因為我們是 \(\tt\),故 \(f\) 的fail指標必然已經求出,我們順著 \(f\) 的fail指標,檢視指向的點是否具有 \(c\) 這個字元的邊。

將當前節點入隊。

來一張 gif 形象地說明一下吧~

圖中紅色節點為隊中節點,黑色節點為已出隊節點。

但是我們發現,在尋找 \(6\) 的字尾節點時,需要跳父節點的fail指標,時間上似乎並不優?可不可以直接取代這個過程呢?

可以。對於 \(x\) 節點,我們只需要將它不存在的兒子設定為它的字尾節點 \(y\) 的相應的兒子。注意到,即使 \(y\) 的相應兒子也不存在也沒關係,因為由上面我們提到過的結論可以知道,這樣的 \(y\) 的兒子一定已經連到 \(y\) 的字尾節點的相應兒子了,以此類推。若是都不存在,那麼最終一定會連到 \(0\) 號節點上。

當然,這樣的節點就沒有入隊的必要了。整個過程就像是壓縮了路徑。

inline void build()  else t[now].ch[i] = t[t[now].fail].ch[i];}}

隨著構建方式的公升級,匹配字串的過程也可以更改了:

inline int query(char *s) 

return res;

}

變得簡單了很多,不是嗎?

不難發現,所有節點與所有的fail邊可以構成一棵樹,我們暫且叫他「\(\tt\) 樹」,不難發現,如果 \(a\) 的字尾節點是 \(b\),且 \(a\) 和 \(b\) 都是單詞的結尾,那麼如果乙個文字在 \(a\) 處有匹配,在 \(b\) 處一定也會有匹配。

所以在 \(b\) 上的匹配總次數就是在 \(b\) 上匹配的次數與在 \(\tt\) 樹上所有 \(b\) 的兒子的匹配總次數之和。

用這個結論可以解決 lg p5357 【模板】ac自動機(二次加強版)。

const int n = 1000010;

const int inf = 0x3fffffff;

struct trie

};struct edge ;

edge e[n];

int ecnt = 1, head[n], end[n];

struct ac_automaton

inline void insert(char *s, int id)

++ t[now].tag;

end[id] = now;

}inline void build() else t[now].ch[i] = t[t[now].fail].ch[i];}}

inline void query(char *s)

}};ac_automaton ac;

inline void add(const int &u, const int &v)

inline void dp(int x)

}int main()

[1] 自動機入門——ac 自動機 - hyl天夢

[2] ac 自動機 - oi wiki

[3] 強勢**ac自動機 - sshwy

字串相關

30 字串相關 30.1追加字元 nsmutablestring string nsmutablestring alloc init nsstring stroneintro info stringbyreplacingoccurrencesofstring withstring 30.3字串比較 ...

字串相關

字串轉換相關部落格 使用stringstream字串轉數字 include include includeusing namespace std int main 使用sscanf 進行字串轉數字char str 1234321 int a sscanf str,d a char str 123.3...

字元 字串 相關解釋

空字元 一般來描述乙個字串的結尾,其實是控制符的一種,但不能理解為沒有字元,應該理解為代表什麼都沒有的字元.好比回車0x0a和換行0x0d雖然不顯示,但是也是控制字元的一種.這些字元以前是用於印表機的,所以很多都沒有用了 字串的概念 在c語言中,字串是指由若干個有效字元 其中包括字母 數字 轉義字元...