学习笔记:AC自动机与动态规划
题目链接
[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)
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];
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);
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);
}
return 0;
}