1. 程式人生 > 其它 >物化檢視全量重新整理與insert的redo生成量測試(69天)

物化檢視全量重新整理與insert的redo生成量測試(69天)

AC自動機(AC automaton)

  • AC自動機,用於處理多模匹配的字串演算法。
  • 可以看作字典樹trie和KMP的結合(一句對會AC自動機和不會的人都沒用的話)

1.trie

每條邊代表一個字元.
有一源點,該點到另外某一點的路徑(路徑也對應唯一結點)即構成一個字串,且為曾經插入過trie的某一字串的字首.

2.AC自動機

對於trie上每一結點,建立一個指向代表其最長字尾的結點的指標,姑且稱為失配(fail)指標. 假設已有一顆trie,接下來討論如何弄出fail指標.
舉個例子,如果有一個由A(代表字串agriculture),指向B(代表字串culture)的指標,則不難發現,A在trie上的父節點\(A^{\prime}\)

(agricultur)也應指向B的父節點\(B^{\prime}\)(cultur).
由此,不難想到一個基於bfs的建立方式:

插入字串&建立fail程式碼
//tot[MAXN]:以該結點結尾的模式串數
//tr[MAXN][26]: 邊
void insert(char *s)//插入字串建立trie
{
	int cur = 0;
	for (int i = 0; s[i]; i++) {
		if (!tr[cur][s[i] - 'a'])
		tr[cur][s[i] - 'a'] = ++cnt;
		cur = tr[cur][s[i] - 'a'];
	}
	tot[cur]++;
}
void build()//構建fail指標
{
	std::queue<int> q;
	memset(fail, 0, sizeof fail);
	for (int i = 0; i < 26; i++)
		if (tr[0][i])
			q.push(tr[0][i]);
	while (!q.empty()) {
		int x = q.front();
		q.pop();
		for (int i = 0; i < 26; i++)
			if (tr[x][i])
				fail[tr[x][i]] = tr[fail[x]][i], q.push(tr[x][i]);
			else//這部分會在下面提及
				tr[x][i] = tr[fail[x]][i];
	}
}

其中,第25行tr[x][i] = tr[fail[x]][i];令人疑惑.
但事實上,可以發現在23行中可能遇到問題:fail[x]並沒有子節點i,這時候,就要繼續查詢fail[fail[x]]是否有子節點i……導致效率變低.
所以,類似於路徑壓縮並查集,便可以讓沒有子節點i的x也指向fail[x]的子節點i. 而且與此同時,在之後的匹配中我們也可以更加方便,而不需要到了葉子結點跳fail.

至此,AC自動機的基本操作————構建fail指標完成.

3.應用

I.多模匹配

洛谷P3808 AC 自動機 簡單版
最基礎的應用,直接在AC自動機上爬就行了. 當走到一個結點時,沿著它的fail一直跳,統計上它所有後綴的貢獻,然後打上標記(因為一個模式串不能算多次),之後跳到標記就停止這樣便可以保證複雜度為\(O(模式串總長)\)

.

匹配程式碼
int match(char *s)
{
	int cur = 0, res = 0;
	for (int i = 0; s[i]; i++) { 
		cur = tr[cur][s[i] - 'a'];
		for (int j = cur; j && ~tot[j]; j = fail[j])
			res += tot[j], tot[j] = -1;//直接把以這個結點結尾的模式串個數改為-1當作標記
	}
	return res;
}

II.拓撲排序優化

洛谷P5357 AC 自動機 二次加強版
類似於上面的匹配,每到一個結點時,就把它和通過fail能到的結點所對應字串(如果是某個模式串結尾)的答案+1.
然鵝,上一題的複雜度保證,來源於通過標記使每個結點至多被訪問一次,但在這題中沒有了這樣的保證(不能打標記),所以暴力跳fail會TLE(不然加強了個寂寞)
繼續觀察fail,發現每次fail,深度不然減少,所以如果把所有fail和結點單獨抽出來,可以得到一張DAG.
所以,可以在爬trie中先將貢獻記在一個結點,最後再通過拓撲排序將貢獻傳給所有通過fail能到達的結點.

主要程式碼
void topo()
{
	static std::queue<int> q;
	for (int i = 0; i <= cnt; i++)
		if (!inDeg[i])
			q.push(i);
	while (!q.empty()) {
		int x = q.front();
		ans[sid[x]] = rec[x];//統計答案,sid指結點對應字串的編號
		q.pop();
		rec[fail[x]] += rec[x];
		if (!--inDeg[fail[x]])
			q.push(fail[x]);
	}
}
void match()
{//比較難看,邊讀入邊處理了
	char ch = getchar();
	while (!(ch >= 'a' && ch <= 'z'))
		ch = getchar();
	int cur = 0;
	for (; ch >= 'a' && ch <= 'z'; ch = getchar())
		cur = tr[cur][ch - 'a'], rec[cur]++;
	topo();
}

III.fail樹

洛谷P3966 [TJOI2013]單詞
此題中,問題變成了考慮一個字串在自己和其他字串中總出現次數,即trie中的一個字串為另外多少個字串的子串.
對於字串\(S\)\(T\),考察\(S\)\(T\)的子串的條件,可以發現子串即為“字首的字尾”,而在AC自動機中:

  • 字首:即\(T\)所代表結點與trie源結點的路徑上某一結點
  • 字尾:即結點通過跳fail所能達到的所有結點
    再次考慮fail指標的性質,可以發現,所有結點fail指標都終將直接/間接指向源結點. 所以,若將所有fail反向,將得到一顆樹,一般稱為fail樹.
    所以,此題中,不難發現對於一個字串,答案即為其在fail樹上的子樹中字串結尾的個數.
主要程式碼
struct AC_automaton
{
	int fail[MAXN], tr[MAXN][26], siz[MAXN], tot = 0, q[MAXN];
	int insert(char *s) 
	{//插入一個字串
		int cur = 0;
		for (int i = 0; s[i]; i++) {
			if (!tr[cur][s[i] - 'a'])
				tr[cur][s[i] - 'a'] = ++tot;
			cur = tr[cur][s[i] - 'a'];
			siz[cur]++; 
		}
		return cur;
	}
	void getFail()
	{//構建fail指標
		int fr = 1, ba = 0;
		for (int i = 0; i < 26; i++)
			if (tr[0][i])
				q[++ba] = tr[0][i];
		while (fr <= ba) {
			int x = q[fr++];
			for (int i = 0; i < 26; i++)
				if (tr[x][i])
					fail[tr[x][i]] = tr[fail[x]][i], q[++ba] = tr[x][i];
				else
					tr[x][i] = tr[fail[x]][i];
		}
	}
	void calc()
	{//統計答案
		for (int i = tot; i; i--)
			siz[fail[q[i]]] += siz[q[i]];
		for (int i = 1; i <= n; i++)
			printf("%d\n", siz[a[i]]);
	}
} acam;

另一題 洛谷P2414 [NOI2011] 阿狸的打字機 也是用fail樹,只要再用資料結構(比如樹狀陣列)簡單維護一下子樹資訊即可.


IV.DP

洛谷P4052 [JSOI2007]文字生成器
考慮反面情況,即總數減去不包含可讀串的個數.

  • 總數:\(26^m\)
  • 不包含可讀串:記\(dp[i][j]\)為串長為\(i\),當前在AC自動機編號為\(j\)的結點,則結果為\(\max_{i = 0}^{tot}\{{dp[i][j]}\}\)
    考慮\(dp[i][j]\)能轉移到的狀態,可以發現,對與下一狀態\((i + 1, tr[j][k])(0\leq k\lt26)\)能轉移當且僅當加上第k個字母時不會出現某一單詞的結尾,即\(tr[j][k]\)不能經過fail跳到一字串的結尾結點,而這可以預處理.
    所以,對於所有這樣合法的\(k\),都可以\(dp[i + 1][tr[j][k]] += dp[i][j]\).
  • 最終結果為 \(26^m-\max_{i = 0}^{tot}\{{dp[i][j]}\}\).
程式碼
#include <bits/stdc++.h>
const int MAXN = 5005, MOD = 10007;
int n, m;
char s[105];
int fpw(int x, int y) {
	int res = 1;
	for (; y; y >>= 1, x = x * x % MOD)
		if (y & 1)
			res = res * x % MOD;
	return res;
}
inline int fplus(int x, int y)
{ return (x + y) >= MOD ? x + y - MOD : x + y; }
inline int fminus(int x, int y)
{ return x >= y ? x - y : x - y + MOD;}
struct AC_automaton
{
	int fail[MAXN], tr[MAXN][26], tot;
	bool ending[MAXN];
	void insert(char *s) 
	{
		int cur = 0;
		for (int i = 0, u; s[i]; i++) {
		    u = s[i] - 'A';
			if (!tr[cur][u])
				tr[cur][u] = ++tot;
			cur = tr[cur][u];
		}
		ending[cur] = true;
	}
	void getFail()
	{
		static std::queue<int> q;
		for (int i = 0; i < 26; i++)
			if (tr[0][i])
				q.push(tr[0][i]);
		while (!q.empty()) {
			int x = q.front();
			q.pop(), ending[x] |= ending[fail[x]];
			for (int i = 0; i < 26; i++)
				if (tr[x][i])
					fail[tr[x][i]] = tr[fail[x]][i], q.push(tr[x][i]);
				else
					tr[x][i] = tr[fail[x]][i];
		}
	}
} ac;
int dp[105][MAXN];//dp[i][j]: 結點j 長度i 的不合法方案數
int main()
{
	scanf("%d %d", &n, &m);
	for (int i = 1; i <= n; i++)
		scanf("%s", s), ac.insert(s);
	ac.getFail();
	dp[0][0] = 1;
	for (int i = 0; i < m; i++)
		for (int j = 0; j <= ac.tot; j++)
			if (dp[i][j])
				for (int k = 0; k < 26; k++)
					if (!ac.ending[ac.tr[j][k]])
						dp[i + 1][ac.tr[j][k]] = fplus(dp[i + 1][ac.tr[j][k]], dp[i][j]);
	int ans = fpw(26, m);
	for (int i = 0; i <= ac.tot; i++)
		ans = fminus(ans, dp[m][i]);
	printf("%d\n", ans);
	return 0;
}

另一題洛谷P3311 [SDOI2014] 數數 類似,是在AC自動機上進行數位dp.