自動機初步

參考資料1
參考資料2

0. 約定

字符串的下標從 \(0\) 開始。\(|s|\) 表示字符串 \(s\) 的長度。
對於字符串 \(s\),記其每一個字符分別爲 \(s_0, s_1, \cdots, s_{|s|-1}\)
子串 \(s_l, s_{l+1}, \cdots, s_{r-1}, s_r\) 簡記爲 \(s[l:r]\)。特別地,若 \(l=0\),可記作 \(s[:r]\);若 \(r=|s|-1\),可記作 \(s[l:]\)
對於字符串 \(a, b\)\(a+b\) 表示拼接操作,即將字符串 \(b\) 拼接到字符串 \(a\) 之後,構成新的字符串。
記構成的新字符串爲 \(c\),則上述拼接操作記爲 \(c\gets a+b\)
其中符號 \(x\gets y\) 表示將 \(y\) 的值賦給 \(x\)
不論是字符還是字符串,皆不加引號。

1. 自動機

首先,我們需要明確自動機的概念。一般來說,OI中我們會接觸到的都是確定有限狀態自動機(DFA)。
DFA可以看成一張有向圖,圖上的節點稱爲狀態(\(Q\)),而從一個狀態到另一個狀態的有向邊稱爲轉移。轉移方式由轉移函數(\(\delta\))進行規定。
DFA有一個起始狀態(\(start\))和若干個接受狀態(\(F\))。當我們把規定字符集(\(\Sigma\))中字符組成的字符串輸入該DFA時,DFA會從起始狀態開始,依照轉移函數,逐個字符進行轉移。
若最終停在了接受狀態,稱該DFA接受該字符串;否則稱該DFA不接受該字符串。
從這裏可以看出,DFA就是用來識別字符串的。
接下來我們對轉移函數進行形式化定義。轉移函數的形式爲 \(\delta(u,c)=v\)。其中 \(u,v\) 都是狀態,而 \(c\) 代表字符串中的字符。
如果我們不能從 \(u\) 狀態由字符 \(c\) 轉移到任何一個狀態,我們稱 \(\delta(u,c)=\text{null}\)\(\text{null}\) 不是接受狀態,且不能轉移到接受狀態,只能轉移到 \(\text{null}\)
我們還可以擴展轉移函數的定義,讓其第二個參數稱爲字符串,此時轉移函數的意義就爲從 \(u\) 狀態開始,依次按照字符串中字符進行轉移,最終到達的狀態。
形式化地,\(\delta(u,s)=\delta(\delta(u,s_0),s[1:])\)

2. 一些簡單的DFA

然而DFA理論並沒有幫助我們解決問題。
因此這裏我們討論一些實際應用中簡單的DFA。

2.1. Trie樹

Trie樹就是最簡單的DFA。
初始狀態就是Trie樹的根,轉移就是Trie樹中的邊,而接受狀態就是你把字符串存進去後打了標記的點。
Trie樹的作用就是識別字符串是否在字典中。

2.2. KMP自動機

KMP算法也可以視作一個自動機。

如圖,我們對字符串 \(\texttt{abaa}\) 建立了一個自動機,其中 \(0\) 是起始狀態,最後的 \(4\) 是接受狀態。
可以看出,整個自動機的結構分爲兩部分,一部分是上面的鏈,另一部分是下面的失配指針。失配指針就是由 \(i\) 指向 \(\pi(i-1)\) 的指針。
形式化地,轉移函數定義爲:
\(\delta(i,c)=\begin{cases}0&s_0\ne c\land i=0\\i+1&s_i=c\\ \delta(\pi(i-1),c)&s_i\ne c\land i>0\end{cases}\)
可以自己嘗試舉例,如 \(\texttt{aaabaaabaa}\),這個自動機應當給出 \(\texttt{abaa}\) 在其中的全部出現。

2.3. 子序列自動機

顧名思義,子序列自動機就是識別字符串子序列的自動機。
也就是說,這個自動機要能夠表示原字符串所有的子序列。
一個顯然的構造方法是令 \(\delta(u,c)\) 爲字符 \(c\)\(u\) 之後第一次出現的位置。
如下圖,我們對 \(\texttt{abcba}\) 建立子序列自動機:

意義如上。可以看出這就包含了原字符串所有的子序列。另外需要注意的是,圖中的 \(0\) 爲起始狀態,而任意一個節點都可以是接受狀態。
一個簡單的構造方法是,從後往前掃,記錄每個字符當前最靠前的出現位置就可以了。
不幸的是,對於這道模板題,我們並不能直接按照上述方式進行構建。
因爲按照上述方式進行構建的複雜度高達 \(O(n|\Sigma|)\),再加上自動機識別的 \(O(\Sigma L)\),顯然不能通過本題。
事實上一般情況下這個複雜度已經夠用了,因爲 \(|\Sigma|\) 通常不會太大。
我們還是需要挖掘 \(\delta\) 的性質。通過觀察我們可以發現,相鄰兩個狀態的轉移函數對於相同的字符集,每次只會有一個值發生變化。
對於上面的例子,我們把 \(\delta(i,\texttt{a}),\ \delta(i,\texttt{b}),\ \delta(i,\texttt{c})\) 用三元組表示:
\((1,2,4)\leftarrow(3,2,4)\leftarrow(3,-1,4)\leftarrow(5,-1,4)\leftarrow(5,-1,-1)\leftarrow(-1,-1,-1)\)
並且發生修改的數值也很容易確定,就是字符 \(s_i\) 對應的值被修改爲了 \(i+1\) 而已。
建立自動機時修改創建新版本,每次自動機進行識別的時候需要訪問歷史版本;
我們自然想到可以使用可持久化線段樹來進行優化。時間複雜度 \(O((n+\Sigma L)\log|\Sigma|)\)
(代碼見字符串算法模板

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