1. 程式人生 > 實用技巧 >AC自動機與動態規劃

AC自動機與動態規劃

題目連結

P4052 [JSOI2007] 文字生成器

P3311 [SDOI2014] 數數

P2292 [HNOI2004] L語言(資料已加強)

[JSOI2007] 文字生成器

計數 DP

很顯然的補集轉換,設不可讀文字數量為 \(sum\)\(Ans = 26^m - sum\)

\(f(i,j)\) 表示長度為 \(i\) 的串中,在 AC 自動機上第 \(j\) 個節點時的最大值。

先建出 Tire 圖,顯然,串中均無法匹配時,有 \(f(i,Tire_{pos\rightarrow j}) = \sum f(i-1,j)\)

\(sum=\sum f(m,i)\)

現在要解決的問題變成了如何判斷串是否無法匹配。

有一個顯然的結論,一個串字尾中有匹配,該串就合法(指能被匹配)。

要處理匹配問題,就要在建 Tire 圖時,記一個數組 \(g_i\) 表示到第 \(i\) 個節點時,該串是否合法。

若一節點為一模式串結尾 \(g_i=1\)

有轉移 \(g_i = g_i \ or \ g_{fail_i}\)

Code(C++):

#include<bits/stdc++.h>
#define forn(i,s,t) for(register int i=(s);i<=(t);++i)
using namespace std;
const int N = 1e4+3,M = 103,Mod = 1e4+7;
inline int q_pow(int p,int k) {
	int Ans = 1;
	while(k) ((k&1)?Ans=Ans*p%Mod:0),p=p*p%Mod,k>>=1;
	return Ans;
}
namespace AC {
	int Tire[N][26],Nxt[N],sl; bool idx[N];
	int f[M][N];
	inline void Ins(char *s) {
		static int p,c;
		p=0;
		for(register int i=0;s[i];++i) {
			c = s[i] - 'A';
			if(!Tire[p][c]) Tire[p][c] = ++sl;
			p = Tire[p][c];
		}
		idx[p] = 1;
	}
	inline void Bld() {
		static queue<int> q;
		forn(i,0,25) if(Tire[0][i]) q.push(Tire[0][i]);
		while(!q.empty()) {
			static int u;
			u = q.front(); q.pop();
			forn(i,0,25) 
				if(Tire[u][i])
					Nxt[Tire[u][i]]=Tire[Nxt[u]][i],q.push(Tire[u][i]),
					idx[Tire[u][i]]|=idx[Tire[Nxt[u]][i]];
				else 
					Tire[u][i] = Tire[Nxt[u]][i];
		}
	}
	inline void solve(int len) {
		f[0][0] = 1;
		forn(i,1,len) forn(j,0,sl) forn(k,0,25)
			if(!idx[Tire[j][k]]) 
				f[i][Tire[j][k]] = (f[i][Tire[j][k]] + f[i-1][j]) %Mod;
		int Ans = q_pow(26,len);
		forn(i,0,sl) Ans = (Ans - f[len][i] + Mod) %Mod;
		printf("%d\n",Ans);
	}
}
int n,m; char s[N];
int main() {
	scanf("%d%d",&n,&m);
	forn(i,1,n) scanf("%s",s),AC::Ins(s);
	AC::Bld();
	AC::solve(m);
	return 0;
}

[SDOI2014] 數數

數位 DP

與上題極其相似,對於 AC 自動機的處理幾乎一模一樣,給出數位 DP 的核心程式碼。

LL f[N][M][2][2];                              // f(dig,pos,lim,zr) 表示
LL dp(int dig,int pos,bool lim,bool zr) {      // 第dig位,與Tire圖上位置為pos的點時,數字是否滿,是否有先導0時的解
	if(!~dig) return !AC::g[pos];
	if(AC::g[pos]) return 0;
	if(~f[dig][pos][lim][zr])
		return f[dig][pos][lim][zr];
	int Lim = lim ? (n[dig] - '0') : 9,Ans = 0;
	forn(i,0,Lim)
		Ans = (Ans + dp(dig-1,(zr&&!i)?0:AC::Tire[pos][i],lim&&(Lim==i),zr&&!i))%Mod;
	return f[dig][pos][lim][zr] = Ans;
}

注意直接這樣 DP 會多算一種為 0 的情況,所以最後的答案要減一。

[HNOI2004] L語言

狀壓 DP

觀察 \(\mid s \mid\) 很小,可以狀壓求解。

\(g_i\) 表示在 Tire 圖中,該節點的狀態 \(s\)\(s\) 的第 \(i\) 位表示到該節點時,前面的第 \(i\) 個節點是否為一個模式串的結尾。

後面只需要一個狀態 \(f\) 進行 DP 即可, \(f\) 的第 \(i\) 位表示在當前位置向前 \(i\) 個字元是否能作為一個合法字首。

那麼如果 \(f \cap g_i \neq \varnothing\) ,則將第 \(0\) 位賦值為 \(1\) ,字串每向後一個字元,狀態 \(f\) 向左移一位。

Code(C++):

#include<bits/stdc++.h>
#define forn(i,s,t) for(register int i=(s);i<=(t);++i)
#define enter putchar('\n')
using namespace std;
const int L = 2e6+3,N = 203;
namespace AC {
	int Tire[N][26],Nxt[N]; unsigned val[N]; bool S[N];
	inline void Ins(char *s) {
		static int sl,p,c;
		p = 0;
		for(register int i=0;s[i];++i) {
			c = s[i] - 'a';
			if(!Tire[p][c]) Tire[p][c] = ++sl;
			p = Tire[p][c];
		}
		S[p] = 1;
	}
	inline void Bld() {
		static queue<int> q,d;
		forn(i,0,25) if(Tire[0][i]) q.push(Tire[0][i]),d.push(1);
		while(!d.empty()) {
			static int u,dep;
			u = q.front(); q.pop();
			dep = d.front(); d.pop();
			val[u] = val[Nxt[u]];
			if(S[u]) val[u] |= 1u<<dep;
			forn(i,0,25)
				if(Tire[u][i])
					Nxt[Tire[u][i]] = Tire[Nxt[u]][i],
					q.push(Tire[u][i]),d.push(dep+1);
				else 
					Tire[u][i] = Tire[Nxt[u]][i];
		}
	}
}
int n,m; char s[N],T[L];
map<string,bool> mp1;
map<string,int> mp2;
int main() {
	scanf("%d%d",&n,&m);
	forn(i,1,n) scanf("%s",s),AC::Ins(s);
	AC::Bld();
	forn(i,1,m) {
		scanf("%s",T);
		if(mp1[T]) {
			printf("%d\n",mp2[T]);
			continue ;
		}
		static int p,Ans; p = Ans = 0;
		static unsigned f; f = 1;
		for(register int i=0;T[i];++i) {
			static int c;
			c = T[i] - 'a';
			p = AC::Tire[p][c];
			f <<= 1;
			if(AC::val[p] & f) {
				f |= 1; Ans = i+1;
			}
		}
		printf("%d\n",Ans);
		mp2[T] = Ans;
	} 
	return 0;
}