1. 程式人生 > 實用技巧 >文字生成器(AC自動機 + DP)

文字生成器(AC自動機 + DP)

這道題,人人都說是AC自動機上dp的套路板子,但是他們給的分析蒟蒻死活也聽不明白(可能是初學的緣故.......)

好久終於搞懂了,寫了這篇題解想造福跟我一樣的同胞(應該只有我一個人這麼菜......)

題意簡化 :

給你\(n\)個模式串,你需要生成一個長度為\(m\)的字串使得至少一個模式串可以匹配成功,問可行的生成方案總數對10007取模。

多串匹配,計數,是dp + AC自動機.....(這裡還是蠻顯然的)

但是怎麼做?

思路

什麼樣的字串使得至少一個模式串可以匹配?這個東西太難處理了。

正難則反 --------- OI中的著名四字成語

不妨轉化為求沒有一個模式串可以匹配成功的方案數為\(sum\)

。(補集轉換)

然後不含一個模式串的字串的方案就是\(26^m - sum\)

首先用\(n\)個模式串模式串建立一個AC自動機

思考什麼時候會有一個文字串使得沒有一個模式串可以匹配成功?
//這是AC自動機進行匹配的程式碼。
//這段函式將會輸出有多少個模式串與文字串匹配成功
void GetAns()
{
	int len = strlen(a),now = 0 , ans = 0;
	for(int i = 0 ; i < len  ; i ++)
	{
		int num = a[i] - 'a';
		now = AC[now].son[num];
		for(int u = now ; AC[u].end != -1 && u ; u = AC[u].Fail)
		{
			ans += AC[u].end;
			AC[u].end = -1;
		}
	}
	cout << ans << endl;
	return ;
}

觀察AC自動機獲取答案的過程,我們發現:

訪問到一個文字串裡面的節點,我們就會不停的跳這個點的\(Fail\),這個點的\(Fail\)\(Fail\) .......(這個就被稱為\(Fail\)鏈),直到跳到根或者是答案已經被計算過的點(已經被跳過了,再往下跳就重複了)。

然後答案累加上以跳到的點為結尾的模式串的個數。

假設\(i\)\(Fail\)指標指向點\(j\),根據\(Fail\)指標的定義就為:Trie上根節點到\(j\)的路徑形成的字串是Trie上根節點到\(i\)的路徑形成的字串的字尾

那麼這樣子答案是顯然可行的.

那麼我們要讓答案為0,怎麼辦?

那就是當前點以及跳到的點上,沒有任何一個模式串以它們為結尾,我們要選的點是這些,至於其他的點,我們則要"避開"。

考慮如何DP

根據套路(沒辦法,套路還是得知道一下的),AC自動機上的DP一般狀態的設定是這樣子的: \(DP[i][j]\) <----- 表示AC自動機上走\(i\)步且最後走的一個是\(j\)的答案

根據上面的分析$DP[i][k] $就要累加上 \(DP[i - 1][j]\) (\(k\)\(j\)的兒子,同時滿足\(j\)沒有一個模式串以其\(Fail\)鏈上的點(包括\(j\))為結尾)

最後,統計出來所有的答案\(\sum_{i = 0}^{i = cnt} {DP[m][i]}也就是以AC自動機上任意一個節點為"j"的答案,同時文字要求長度為m\)

答案就是\((26^m - sum)\) \(mod\) \(10007\)

至此結束.

注意一下模意義下減法要加上\(Mod\)防止變成負數,詳見程式碼。

Code

#include <bits/stdc++.h>
using namespace std;
int n,m,cnt = 0;
const int MAXN = 6005,MAXM = 105,Mod = 10007;//常量賦值
char s[1005];//給定的模式串用這個存
struct node{
	int end,Fail;
	int son[26];
}AC[MAXN];//AC自動機
int vis[MAXN];//建立Fail指標的時候要用的東西
int f[MAXM][MAXN];//DP陣列
void build()
{
	int len = strlen(s),now = 0;
	for(int i = 0 ; i < len ; i ++)
	{
		int num = s[i] - 'A';
		if(AC[now].son[num] == 0)
			AC[now].son[num] = ++cnt;
		now = AC[now].son[num];
	}
	AC[now].end = 1;
}//建立AC自動機

void GetFail()
{
	int now = 0 , head = 0 , tail = 0;
	for(int i = 0 ; i < 26 ; i ++)
		if(AC[0].son[i])
			tail ++ , vis[tail] = AC[0].son[i];
	while(head < tail)
	{
		head ++;
		int v = vis[head];
		for(int i = 0 ; i < 26 ; i ++)
		{
			if(AC[v].son[i])
			{
				AC[AC[v].son[i]].Fail = AC[AC[v].Fail].son[i];
				tail ++;
				vis[tail] = AC[v].son[i];//普通的建立AC自動機即可
				AC[AC[v].son[i]].end |= AC[AC[AC[v].son[i]].Fail].end;//這裡運用了或運算來求出Fail鏈上是否有一個點為模式串的結尾
			}
			else AC[v].son[i] = AC[AC[v].Fail].son[i];
		}
	}
	return ;
}

int quick_power(int x,int y){
	int ans = 1 , op = x;
	if(y == 2)return x*x;
	if(x == 0)return 0;
	while(y){
		if(y % 2 == 1)ans *= op , ans %= Mod;
		op *= op , op %= Mod;
		y = y >> 1;
	}
	return ans % Mod;
}

void DP()
{
	f[0][0] = 1;
	for(int i = 1 ; i <= m ; i ++)
		for(int j = 0 ; j <= cnt ; j ++)
			if(!AC[j].end)//我們顯然不能對不合法的點進行動態規劃
			{
				for(int k = 0 ; k < 26 ; k ++)
				f[i][AC[j].son[k]] =( f[i][AC[j].son[k]] + f[i - 1][j] )% Mod;
			}
	int ans = 0;
	for(int j = 0 ; j <= cnt ; j ++)
		if(!AC[j].end)ans += f[m][j],ans %= Mod;
	cout <<(quick_power(26,m) - ans + Mod )% Mod;//這裡要加上Mod,不然會死
}

int main()
{
	cin >> n >> m;
	for(int i = 1 ; i <= n ; i ++)
	{
		cin >> s;
		build();
	}
	GetFail();//這裡是進行建Fail的
	DP();//這裡是進行DP的
	return 0;
}