廣義字尾自動機(廣義SAM)
參考部落格:【學習筆記】字串—廣義字尾自動機
基礎部分:字尾自動機(SAM)
廣義字尾自動機適用於多串的子串問題。它的DFA可以識別多串中的任意一個子串。同時也有類似 SAM 的一些性質。
知識精要
廣義SAM 的建立
根據參考部落格所說,有好幾種 假 寫法。比如:每一個串的開頭設定 \(lst\) 為 \(1\);多串拼成一個大串,中間用'#'連線 等等;
正規寫法:
離線做法
建出所有串的 \(Trie\) 樹。然後在 \(Trie\) 上 \(bfs\) ,每次將一個字元加到其 \(Trie\) 的 \(fa\) 上。其餘與 SAM 同。
\(t_c[cur]:Trie\) 上的 \(cur\) 節點的父親節點
\(pos[cur]:Trie\) 上的 \(cur\) 節點(所代表子串)在 SAM 上的位置
\(int ins(int c, int lst):\) 幾與普通SAM同。傳入 \(lst\),傳出 \(np\)
\(Code:\)
inline void work() { for (register int c = 0; c < 26; ++c) if (trie[1][c]) que[++rear] = trie[1][c]; pos[1] = 1; while (front < rear) { int cur = que[++front]; pos[cur] = ins(t_c[cur], pos[t_fa[cur]]); for (register int c = 0; c < 26; ++c) if (trie[cur][c]) que[++rear] = trie[cur][c]; } }
線上做法
每一個串的開頭設定 \(lst\) 為 \(1\)。但是由於一開始可能就存在 \(son[lst][c]\),那就不太妙。因此需要加兩個特判:
特判一
if (son[lst][c] && len[son[lst][c]] == len[lst] + 1) return son[lst][c];
如果有一個完美的節點 \(q\),那麼我們直接將 \(lst\) 設定為這個點返回即可。
特判二
//... //if (len[q] == len[p] + 1) return ... if (p == lst) flag = true; //int nq = ++tot; len[nq]...
如果存在一個不那麼完美的節點 \(q\),那麼我們可以新建一個節點 \(nq\),然後...(同SAM)。這樣我們就和普通 SAM 合併一下,在最後判斷一下,如果是這種情況,就直接返回 \(nq\) 即可。可以證明的是,此時建立的 \(p\) 節點是一個“空點”,因為 \(len[p] = len[lst] + 1, minlen[p] = len[nq] + 1 = len[lst] + 2\)。
程式碼
inline int ins(int c, int lst) {
if (son[lst][c] && len[son[lst][c]] == len[lst] + 1) return son[lst][c];
int np = ++tot, p = lst;
len[np] = len[p] + 1;
while (p && !son[p][c]) son[p][c] = np, p = fa[p];
if (!p) return fa[np] = 1, np;
int q = son[p][c];
if (len[q] == len[p] + 1) return fa[np] = q, np;
bool flag = false;
if (p == lst) flag = true;
int nq = ++tot; len[nq] = len[p] + 1;
fa[nq] = fa[q], fa[q] = fa[np] = nq;
memcpy(son[nq], son[q], sizeof(son[q]));
while (p && son[p][c] == q) son[p][c] = nq, p = fa[p];
return flag ? nq : np;
}
線上做法由於空間開得小,碼量也小,一般使用線上做法,但是離線做法的思想也要知道。
在建立廣義SAM的同時,還可以維護每個串的 \(endpos\) 集合,應用 \(len\) 的性質等等。具體見下方。
技能展示
識別某串是否是多串中某串的子串
直接建廣義SAM,在DFA上跑即可。
多串中本質不同子串個數
建廣義SAM,\(ans = \sum{len[i] - len[fa[i]]}\)
維護每個串的 \(endpos\) 集合
在加入(\(ins\))時,對於表示當前串的節點 的\(siz\) 賦值為 1。(注意特判的地方,如果找到了個“完美的替身”,那麼讓替身的 \(siz\) 為1;如果確實要讓nq代替np,那麼將np的siz的那個1轉移給nq)然後再按照類似普通SAM的做法搞出 \(endpos\) 集合。(搞\(firstpos\)後線段樹合併)
求多串最長公共子串
SP1812 LCS2 - Longest Common Substring II
建立多串的廣義SAM,同時維護每個串的 \(endpos\) 集合(大小)。最後檢查每個節點是否“被所有節點共同擁有”。如果有這樣的節點,那麼可以用這個節點的 \(len\) 更新答案。
例題
#3473. 字串
調到心態爆炸,暫時放棄。
記錄在這裡:my record
附
模板(除錯用)
inline int ins(int c, int lst) {
if (son[lst][c] && len[son[lst][c]] == len[lst] + 1) return son[lst][c];
int np = ++tot, p = lst;
len[np] = len[p] + 1;
while (p && !son[p][c]) son[p][c] = np, p = fa[p];
if (!p) return fa[np] = 1, np;
int q = son[p][c];
if (len[q] == len[p] + 1) return fa[np] = q, np;
bool flag = false;
if (p == lst) flag = true;
int nq = ++tot; len[nq] = len[p] + 1;
fa[nq] = fa[q], fa[q] = fa[np] = nq;
memcpy(son[nq], son[q], sizeof(son[q]));
while (p && son[p][c] == q) son[p][c] = nq, p = fa[p];
return flag ? nq : np;
}
Continued...