「Wallace 筆記」序列自動機入門
基本概念
記號
同為 DFA 的記號。
- \(\Sigma\):字符集;
- \(Q\):狀態集;
- \(q_0(\in Q)\):起始狀態;
- \(F(\in Q)\):接受狀態集;
- \(\delta\):轉移函式。
-
- \(\delta(x, c)\) 表示狀態 \(x\) 的字元 \(c\) 的轉移。若無記為 \(\text{null}\)
-
- \(\delta(x, S)\) 其中 \(S\) 為字串,等價於 \(\delta(\delta(x, S[1, |S| - 1]), S_{|S|})\)
定義
序列自動機是接受且僅接受一個字串的子序列的自動機。
如當 \(\Sigma = \{ \texttt{a, b, c} \}\)
狀態
對字串 \(S\) 構建序列自動機,那麼這個自動機中存在 \(|S| + 1\) 個狀態。
對於字串 \(T\),狀態 \(\delta(q_0, T)\) 表示 \(T\) 作為 \(S\) 的子序列,在 \(S\) 中第一次出現的末端位置。
若 \(\delta(q_0, T) = \text{null}\),則說明 \(T\) 不是 \(S\) 的子序列。
根據定義,序列自動機上的所有狀態都是接受狀態(\(F = Q\))。
轉移函式
轉移函式 \(\delta\) 的設計,我們可以貪心地認為 \(\delta(x, c)\)
但很顯然之後可能會有多個 \(c\),為什麼選取最前面的呢?假如說 \(i < j\),那麼字尾 \(S[j, |S|]\) 中的所有子序列一定被完全包含在後綴 \(S[i, |S|]\) 中。換句話說,\(S[j, |S|]\) 的所有子序列的集合為 \(S[i, |S|]\) 所有子序列的子集。於是不難發現後面的一定不會優於前面。
形式化地講,\(\delta(x, c) = \min\{ y | S_y = c, y > x \}\)。如果不存在不妨設為 \(-1\) 表示 \(\text{null}\)
構造演算法
樸素演算法
首先,根據上文對轉移函式的討論,我們可以從後往前掃,逐位構建。
對於狀態 \(i\),假如已經求出了 \(\delta(i, c)\) 的轉移,考慮如何推出狀態 \(i - 1\) 的轉移函式。
不難發現,只有一個位置,即 \(S_i\) 的值的位置上,是需要變動的,那麼:\(\delta(i - 1, S_i) = i\)。
其他字元的轉移,只需要直接複製下來即可。這樣的演算法的時空複雜度為 \(O(|S| \times |\Sigma|)\)。
程式碼非常簡單:
for (register int i = 1; i <= m; i++) // 最後一位的所有轉移置為 null
next[n][i] = -1;
for (register int i = n; i; i--) { // 倒序逐位構造
for (register int j = 1; j <= m; j++) // 先複製前一輪的結果
next[i - 1][j] = next[i][j];
next[i - 1][dat[i]] = i; // 更新當前字元的轉移
}
可持久化陣列優化
上一個演算法的效率在 \(\Sigma\) 大小過大時都不夠優秀,問題在於對前面狀態複製了過多資訊。
然而我們只需要改變一個位置的值,把整個賦值顯然是浪費。
這就需要 可持久化 的思想了——我們先對最後一個建出線段樹,初始值為 \(-1\)。
接下來在構建時只需在一個位置更新即可。時空複雜度 \(O(|S|\log |\Sigma|)\)。較之前都有較大的提高。
struct segt_node {
segt_node* lc;
segt_node* rc;
int l, r, val;
#define mid ((this->l + this->r) >> 1)
segt_node(segt_node *last, int pos, int _val) : l(last->l), r(last->r), val(0) {
if (this->l == this->r) { val = _val; return; }
if (pos <= mid) {
rc = last->rc;
lc = new segt_node(last->lc, pos, _val);
} else {
lc = last->lc;
rc = new segt_node(last->rc, pos, _val);
}
}
segt_node(int L, int R) : l(L), r(R), val(-1) {
if (l == r) return;
lc = new segt_node(l, mid);
rc = new segt_node(mid + 1, r);
}
int at(int pos) {
if (this->l == this->r) return this->val;
if (pos <= mid) return this->lc->at(pos);
else return this->rc->at(pos);
}
#undef mid
} *root[N];
// main 函式內:
root[n] = new segt_node(1, m);
for (register int i = n; i; --i)
root[i - 1] = new segt_node(root[i], dat[i], i);
二分查詢(非顯式構建)
其實還有一種不需要顯式構造出的方法。我們建出自動機,實際上就是需要快速計算出轉移函式 \(\delta\)。
但假如我們並沒有構造出自動機,不過仍能高效得到轉移函式的值,好像並不是不可以。
這裡就有一種非常方便的方法:二分查詢。
我們先預處理出 \(\Sigma\) 中每一種值的出現位置,這一可以用 vector
實現。為方便討論,記 \(p(i)\) 為字元 \(i\) 在字串中的出現位置的集合。
在做轉移時,假設要得到 \(\delta(x - 1, c)\),那麼只要 找出集合 \(p(c)\) 中第一個 \(\ge c\) 的元素。
由於我們預處理 \(p\) 集合時就是自然有序的,因此只需要二分查詢即可(upper_bound
)。
這樣的空間可以達到 \(O(|S|)\)。
習題
- Luogu P3500 [POI2010]TES-Intelligence Test:https://www.luogu.com.cn/problem/P3500
- Luogu P4112 [HEOI2015]最短不公共子串:https://www.luogu.com.cn/problem/P4112
後記
- 原文地址:https://www.cnblogs.com/-Wallace-/p/13297178.html
- 本文作者:@-Wallace-
- 轉載請附上出處。
reference: