1. 程式人生 > 實用技巧 >廣義字尾自動機(廣義SAM)

廣義字尾自動機(廣義SAM)

參考部落格:【學習筆記】字串—廣義字尾自動機

基礎部分:字尾自動機(SAM)

廣義字尾自動機適用於多串的子串問題。它的DFA可以識別多串中的任意一個子串。同時也有類似 SAM 的一些性質。

知識精要

廣義SAM 的建立

模板提交處

根據參考部落格所說,有好幾種 寫法。比如:每一個串的開頭設定 \(lst\)\(1\);多串拼成一個大串,中間用'#'連線 等等;

正規寫法:

離線做法

例題:P3346 [ZJOI2015]諸神眷顧的幻想鄉

建出所有串的 \(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...