经典AC自动机DP
CF808G Anthem of Berland
给定 \(s,t\) 串,\(s\) 串中有问号,问 \(t\) 在 \(s\) 中的最大出现次数。
\(|s|\times |t| \leq 10^7\)。
题解
AC自动机经典题,直接 \(dp(i,j)\) 表示前 \(i\) 个点,AC自动机状态是 \(j\) 的最大出现次数。
枚举出边转移就完事。时间复杂度 \(O(26nm)\)。
有个常数优化,可以把 \(O(26)\) 去掉。
注意到AC自动机每个状态的出边只有一条能转移到儿子,其余的都是沿着fail跳了若干步再转移。
那么我们可以把跳fail的过程放到DP里。方法是每次chkmax(dp[i-1][fail[j]],dp[i-1][j])
然后只转移那条能到儿子的边。
CO int N=1e5+10;
char s[N],t[N];
int fa[N],dp[N];
int main(){
scanf("%s",s+1);
int n=strlen(s+1);
scanf("%s",t+1);
int m=strlen(t+1);
for(int i=2;i<=m;++i){
int j=fa[i-1];
while(j and t[j+1]!=t[i]) j=fa[j];
fa[i]=j+(t[j+1]==t[i]);
}
fill(dp,dp+m+1,-1),dp[0]=0;
for(int i=1;i<=n;++i){
for(int j=m;j>=0;--j)if(dp[j]!=-1){
if(j>0) dp[fa[j]]=max(dp[fa[j]],dp[j]);
if(j<m and (s[i]=='?' or s[i]==t[j+1])) dp[j+1]=dp[j];
if(j>0) dp[j]=-1;
}
if(dp[m]!=-1) ++dp[m];
}
printf("%d\n",*max_element(dp,dp+m+1));
return 0;
}
密码
众所周知,密码在信息领域起到了不可估量的作用。对于普通的登陆口令,唯一的破解 方法就是暴力破解一逐个尝试所有可能的字母组合,但这是一项很耗时又容易被发现的工 作。所以,为了获取对方的登陆口令,在暴力破解密码之前,必须先做大量的准备工作。经 过情报的搜集,现在得到了若干有用信息,形如:
“我观察到,密码中含有字符串***。”
例如,对于一个10位的密码以及串hello与world,可观察到的字符能的密码组合为 helloworld与worldhello;而对于6位的密码以及观察到的字符串good与day,可能的 密码组合为gooday。
有了这些信息,就能够大大地减少尝试的次数了。请编一个程序,计算所有密码组合的可能。密码中仅可能包含a - z之间的小写字母。
对于100%的数据,1<=L<=25,1<=N<=10,每个观察到的字符串长不超过10,并且保证输出结果小于2^63。
LadyLex的题解
我们首先考虑:对于串\(i\)和\(j\),如果\(j\)是\(i\)的子串,那么我们根本不用考虑最初单独插入进来的\(j\)串,因为只要\(i\)串存在,\(j\)串就一定存在
那么我们可以在构建出AC自动机之后,把每个节点从fail指针能达到的节点都设为”不是单词节点“,最后再给单词节点重新编号即可。
那么接下来,我们考虑dp的过程。由于节点数,串数和串长都很小,所以我们考虑状态压缩来解决这个问题。
我们定义状态数组\(f[i][j][k]\)表示当前串长为\(i\),位于\(j\)号节点,模式串出现情况为\(k\)的方案数。
(这种"走\(i\)步到达\(j\)节点”也是AC自动机上的常见套路之一)
那么我们事先把单词节点对应的串用二进制压好,转移到时候我们只需要这样处理即可:
f[i+1][ch[j][u]][k|val[ch[j][u]]]+=f[i][j][k];
这样我们就可以搜出方案数,接下来我们考虑输出小于42的具体方案。
首先我们可以得到一个性质:若总方案数不超过42,那么最终串一定仅由给定串拼接而成。
因为如果随机字母可以存在,哪怕只有1个模式串,并且仅有1个随机字母,合法方案数在这种最小情况下也有2×26=52种>42
因此我们只需要用搜索进行一个dp的逆过程,看合法方案由哪个节点转移过来,并且记录一路上经过的字符,最后排序输出即可。
这真是一道很的题目,方式以及套路很经典,对于状压和搜索的应用都很灵活!
UPD:这题强行组合了两种套路。
时间复杂度\(O(26 L N^2 2^N)\),算出来是7e7。
co int N=11,L=26,K=(1<<10)+10;
int l,n;
char s[N][N];
namespace AC
{
int tot,num;
int ch[N*N][26],fail[N*N];
int val[N*N],meaning[N*N];
void ins(char s[],int n)
{
int u=0;
for(int i=0;i<n;++i)
{
int k=s[i]-'a';
if(!ch[u][k])
ch[u][k]=++tot;
u=ch[u][k],meaning[u]=k;
}
val[u]=1;
}
void getfail()
{
std::queue<int>Q;
for(int i=0;i<26;++i)
if(ch[0][i])
Q.push(ch[0][i]);
while(Q.size())
{
int u=Q.front();Q.pop();
for(int i=0;i<26;++i)
{
if(ch[u][i])
{
fail[ch[u][i]]=ch[fail[u]][i];
Q.push(ch[u][i]);
}
else
ch[u][i]=ch[fail[u]][i];
}
}
for(int i=1;i<=tot;++i)
for(int u=fail[i];u;u=fail[u])
val[u]=0;
for(int i=1;i<=tot;++i)
if(val[i])
val[i]=(1<<num++);
}
ll f[L][N*N][K];
struct sol
{
char s[L];
sol()
{
memset(s,0,sizeof s);
}
void print()
{
puts(s);
}
bool operator<(co sol&b)co
{
for(int i=0;i<l;++i)
if(s[i]!=b.s[i])
return s[i]<b.s[i];
return 0;
}
}stack;
std::vector<sol>str;
void dfs(int len,int i,int state,int now)
{
stack.s[len-1]=now+'a';
if(len==1)
{
str.push_back(stack);
return;
}
for(int j=0;j<=tot;++j)
if(f[len-1][j][state]&&ch[j][now]==i)
dfs(len-1,j,state,meaning[j]);
if(val[i])
for(int j=0;j<=tot;++j)
if(f[len-1][j][state^val[i]]&&ch[j][now]==i)
dfs(len-1,j,state^val[i],meaning[j]);
}
void getsolution()
{
for(int i=1;i<=tot;++i)
if(f[l][i][(1<<num)-1])
dfs(l,i,(1<<num)-1,meaning[i]);
}
void solve()
{
f[0][0][0]=1;
for(int i=0;i<l;++i)
for(int j=0;j<=tot;++j)
for(int k=0;k<(1<<num);++k)
if(f[i][j][k])
for(int u=0;u<26;++u)
f[i+1][ch[j][u]][k|val[ch[j][u]]]+=f[i][j][k];
ll ans=0;
for(int j=0;j<=tot;++j) // edit 1:0
ans+=f[l][j][(1<<num)-1];
printf("%lld\n",ans);
if(ans<=42)
{
getsolution();
sort(str.begin(),str.end());
assert(str.size()==ans);
for(int i=0;i<ans;++i)
str[i].print();
}
}
}
int main()
{
read(l),read(n);
for(int i=1;i<=n;++i)
{
scanf("%s",s[i]);
AC::ins(s[i],strlen(s[i]));
}
AC::getfail();
AC::solve();
return 0;
}
文本生成器
给出若干个由大写字母构成的单词,问长度为 m ,由大写字母构成的字符串中,包含至少一个单词的数目.对 10007 取模.
jklover的题解
可以先求出不包含任意一个单词的字符串数目,再用总数目26m减去.
将单词建成一个 AC 自动机,类似上题,合并权值即可求出一个节点是否能被走到.
用 \(f[i][j]\) 表示已经走了 \(i\) 步,走到了节点 \(j\) 时的方案数. \(O(n^2)\) dp 即可.
AC自动机上面dp才是AC自动机的精髓。
co int mod=1e4+7;
int add(int x,int y)
{
return (x+y)%mod;
}
int mul(int x,int y)
{
return x*y%mod;
}
int qpow(int x,int k)
{
int res=1;
while(k)
{
if(k&1)
res=mul(res,x);
x=mul(x,x),k>>=1;
}
return res;
}
co int N=7777,S=26;
int n,m;
namespace AC
{
int idx;
int ch[N][S],fail[N],val[N];
int f[101][N];
void init()
{
memset(f,-1,sizeof f);
}
void ins(char*s,int len)
{
int u=0;
for(int i=0;i<len;++i)
{
int k=s[i]-'A';
if(!ch[u][k])
ch[u][k]=++idx;
u=ch[u][k];
}
val[u]=1;
}
void getfail()
{
std::queue<int>Q;
for(int i=0;i<S;++i)
if(ch[0][i])
Q.push(ch[0][i]);
while(Q.size())
{
int u=Q.front();Q.pop();
for(int i=0;i<S;++i)
{
if(ch[u][i])
{
fail[ch[u][i]]=ch[fail[u]][i];
Q.push(ch[u][i]);
}
else
ch[u][i]=ch[fail[u]][i];
}
val[u]|=val[fail[u]];
}
}
int dfs(int i,int j)
{
if(f[i][j]!=-1)
return f[i][j];
if(val[j])
return 0;
if(i==m)
return 1;
int&res=f[i][j]=0;
for(int k=0;k<S;++k)
res=add(res,dfs(i+1,ch[j][k]));
return res;
}
void solve()
{
int ans=qpow(26,m);
ans=add(ans,mod-dfs(0,0));
printf("%d\n",ans);
}
}
char buf[N];
int main()
{
AC::init();
read(n),read(m);
for(int i=1;i<=n;++i)
{
scanf("%s",buf);
AC::ins(buf,strlen(buf));
}
AC::getfail();
AC::solve();
return 0;
}
Rescue the Rabbit
现在有n个基因片段(用包含A、G、T、C的字符串表示),每个基因片段有一个权值,现在求长为L的基因的最大权值(每个基因片段重复出现算一次,不用计算多次)?
n (1 ≤ n ≤ 10),l (1 ≤ l ≤ 100)
分析
未知定长串中不同已知模板串的出现次数问题,一般做法是AC自动机上dp。
考虑背包,\(dp(i,j,k)\)表示当前串长为\(i\),在AC自动机上对应节点\(j\),已匹配的模板串的状态为\(k\)的情况是否出现。用刷表法向后转移。先枚举不定串长度,再枚举AC自动机上节点,然后枚举已知状态,最后枚举字母边转移。
时间复杂度\(O(l \cdot MaxNode \cdot 2^n \cdot SigmaSize)\)。第一维可以滚动,空间复杂度\(O(MaxNode \cdot 2^n)\)
const int MAXN=1010;
const int SigmaSize=4;
bool dp[2][MAXN][1100];
int mp[15];
struct Trie
{
int next[MAXN][SigmaSize];
int fail[MAXN];
int end[MAXN];
int root,ncnt;
int newnode()
{
for(int i=0;i<SigmaSize;++i)
next[ncnt][i]=-1;
end[ncnt++]=0;
return ncnt-1;
}
void init()
{
ncnt=0;
root=newnode();
}
int id(char c)
{
if(c=='A')
return 0;
else if(c=='G')
return 1;
else if(c=='T')
return 2;
else
return 3;
}
void insert(char*str,int v)
{
int now=root;
int len=strlen(str);
for(int i=0;i<len;++i)
{
int c=id(str[i]);
if(next[now][c]==-1)
next[now][c]=newnode();
now=next[now][c];
}
end[now]|=(1<<v);
}
void getfail()
{
queue<int>Q;
fail[root]=root;
for(int i=0;i<SigmaSize;++i)
{
if(next[root][i]==-1)
next[root][i]=root;
else
{
fail[next[root][i]]=root;
Q.push(next[root][i]);
}
}
while(!Q.empty())
{
int now=Q.front();
Q.pop();
end[now]|=end[fail[now]];
for(int i=0;i<SigmaSize;++i)
{
if(next[now][i]==-1)
next[now][i]=next[fail[now]][i];
else
{
fail[next[now][i]]=next[fail[now]][i];
Q.push(next[now][i]);
}
}
}
}
void solve(int n,int l)
{
memset(dp,0,sizeof(dp));
dp[0][0][0]=1; // dp[len][node][state]
int cur=1;
for(int i=1;i<=l;++i)
{
memset(dp[cur],0,sizeof(cur));
for(int j=0;j<ncnt;++j)
{
for(int k=0;k<(1<<n);++k)
{
for(int q=0;q<SigmaSize;++q)
{
int nxt=next[j][q];
dp[cur][nxt][k|end[nxt]]=(dp[cur][nxt][k|end[nxt]]||dp[cur^1][j][k]);
}
}
}
cur^=1;
}
int ans=-INF;
for(int i=0;i<ncnt;++i)
for(int j=0;j<(1<<n);++j)
{
if(dp[cur^1][i][j])
{
int sum=0;
for(int k=0;k<n;++k)
if(j&(1<<k))
sum+=mp[k];
ans=max(ans,sum);
}
}
if(ans<0)
printf("No Rabbit after 2012!\n");
else
printf("%d\n",ans);
}
}AC;
char s[110];
int main()
{
int n,l,w;
while(~scanf("%d %d",&n,&l))
{
AC.init();
for(int i=0;i<n;++i)
{
scanf("%s %d",s,&w);
AC.insert(s,i);
mp[i]=w;
}
AC.getfail();
AC.solve(n,l);
}
return 0;
}