AC自动机-题解集合
P4052 [JSOI2007]文本生成器
题意:
给定一堆短字符串,再给定 \(m\) 长度的随机字符串(小写字符),求短字符串在随机字符串中出现的次数,对 \(10007\) 取模。
分析:
这种关于字符串出现次数之类的题目,基本上都是 \(AC\) 自动机。
有人说过,\(AC\) 自动机的 \(DP\) 都很套路。
\(dp[i][j]\) 表示当前在节点 \(j\) ,且串长度为 \(i\) 的情况,有时候再加一维表示这个状态里面包含了哪些东西。
而且 \(AC\) 自动机的 \(DP\) 经常用矩阵乘法优化。
我们发现,找不识别的比找识别的好实现。因此进行容斥。
定义 \(dp[i][j]\) 表示当前在 \(j\) 点且串长为 \(i\) 时不经过单词结尾的路径条数,然后从父亲往儿子转移,即:
if(!val[c[j][k]])//不存在单词结尾,就可进行转移
dp[i][c[j][k]]+=dp[i-1][j];
初始化为 \(f[0][0]=1\).
注意一个单词的后缀是一个可读的单词,那么这个单词一定也是可读的,我们就不能继续往这个单词走了,也就是上面代码的 \(if\) 部分。
最后统计在长度为 \(m\) 的情况下,在节点 \([1,cnt]\) 各有多少不存在的情况,加和即可。
P3311 [SDOI2014] 数数
题意:
给定一些数字串,再给定一个 \(n\) ,求在 \([1,n]\) 的数中不存在这些数字串的数 的个数。
分析:
其实这题就是上一题的增强版,这里有了数位限制,即使用数位 \(dp\) 来计算个数。
当我们转移到数字串的末尾时,是非法的,但是我们并不清楚每个串插入的地方是违法的。
此时,可以有转移:\(val[y]|=val[fail[y]]\)。
关于数位 \(dp\) ,还有需要注意的:
- 反转匹配数组,
- 要注意没有前导零的时候不能转移到 Trie 上的其他节点,因为这个时候我们的串还没开始。
关键部分代码:
int dfs(int now,int pos,int limit,int lead) {//位数,位置,最高位,前导0
if(now <= 0) return !val[pos];
if(val[pos]) return 0;//不能访问
if(f[now][pos][limit][lead]!=-1) return f[now][pos][limit][lead]; //数位dp返回
int top=limit?(m[now]-'0'):9,res=0;//限制位数,记录答案
for(int i=0;i<=top;i++){
(res+=dfs(now-1,
(lead&&(i==0))?0:c[pos][i]/*我们的串还没开始*/
(limit&&(i+'0'==m[now])),//限制位数
(lead&&(i==0))))%mod;//前导0
}
return f[now][pos][limit][lead]=res;
}
int main(){
........
reverse(m+1,m+1+len);//反转数组,因为玄学下标的出现(没有得到解释)
memset(f,-1,sizeof(f));
int ans = dfs(len,0,1,1);
ans=(ans+mod-1)%mod;
}
[TJOI2013]单词
题意:
一堆字符串,求每个字符串在这堆串中出现了多少次。
分析:
这里涉及到了 \(Fail\) 指针的定义:指向树上该字符串最长后缀的末尾。
也就是当跑完一个字符串时,我们会到其他相同字符串的末尾,这样就增加了一次答案。
这样,我们就有思路:每一个节点记录属于多少个字符串,为他的权值。
一个节点表示的字符串表示在整个字典中出现的次数 \(=\) 在 \(Fail\) 树中的子树的权值的和。
代码:
void add(int x){
......
for(int i=1;i<=len;i++){
int u=s[i]-'a';
if(!c[now][u]) c[now][u]=++cnt;
now=c[now][u];
val[now]++;//点权值
}
a[x]=now;//记录末尾
}
void build(){
int head=0,tail=0;
for(int i=0;i<26;i++) if(ch[0][i]) q[++tail]=ch[0][i];
while(head<tail){
int x=q[++head];
for(int i=0;i<26;i++){
if(ch[x][i]){
q[++tail]=ch[x][i];
fail[ch[x][i]]=ch[fail[x]][i];
}
else ch[x][i]=ch[fail[x]][i];
}
}
}
void solve(){
for(int i=cnt;i>=0;i--) val[fail[q[i]]]+=val[q[i]];
for(int i=1;i<=n;i++) printf("%d\n",val[a[i]]);
}
[COCI2015]Divljak
题目大意:
一开始 \(n\) 个字符串集合,有 \(Q\) 个操作:
- 集合中插入一个新串
- 询问给定字符串是多少个集合中的字符串的子串。
思路:
肯定是先建立 \(AC\) 自动机,记录每个字符串的结束位置。
将 \((fail[x],x)\) 建树,然后将这棵树求出每个点的 \(dfs\) 序。
利用树上差分思想,把要插入的 \(T\) 集合在 \(trie\) 树中统计走过的节点,记录一下。
再按 \(dfn\) 排序,将相邻两个节点的在树上的位置 \(+1\) ,表示多一个串匹配。
但是他们的 \(lca\) 和 \(lca\) 的祖先很明显是 \(+2\) 串匹配,不符合此串在集合中的出现次数,因此 \(lca\) 位置标记 \(-1\).
对于此过程,运用树状数组统计答案,树链剖分后,按每个节点的 \(dfs\) 序维护,同时也可以跑 \(lca\) .
出现次数就是 其节点和其子树的和
#include<bits/stdc++.h>
using namespace std;
#define lowbit(x) x&-x
const int N=2e6+5,M=1e5+5;
int c[N][26],val[N],fail[N],cnt;
int nxt[N],ver[N],head[N],tot;
int fa[N],dfn[N],dep[N],sizes[N],son[N],top[N],rk[N],dfstime;
int n,Q,a[N],treesum[N]; char str[N];
void add(int x,int y){
ver[++tot]=y; nxt[tot]=head[x]; head[x]=tot;
}
void ins(char *s,int id){
int len=strlen(s);int now=0;
for(int i=0;i<len;i++){
int x=s[i]-'a';
if(!c[now][x]) c[now][x]=++cnt;
now=c[now][x];//指向地址
}
val[id]=now;//单词末尾
}
void build(){
queue<int> q;
for(int i=0;i<26;i++)//第一层是根节点0
if(c[0][i])//遍历第二层,搜索存在的子树
fail[c[0][i]]=0,q.push(c[0][i]);//fail指向根节点
while(!q.empty()){
int x=q.front();q.pop();
for(int i=0;i<26;i++){//搜索这一棵树
if(c[x][i]){
fail[c[x][i]]=c[fail[x]][i];//让这个节点的失败指针指向(((他父亲节点)的失败指针所指向的那个节点)的下一个节点)
q.push(c[x][i]);//存在子树,就压入队列
}
else
c[x][i]=c[fail[x]][i];//否则就让这个子节点指向当前节点fail指针的子节点
}
}
}
void dfs1(int x,int father){
sizes[x]=1;
fa[x]=father;
dep[x]=dep[father]+1;
for(int i=head[x];i;i=nxt[i]){
int y=ver[i]; if(y==father) continue;
// fa[y]=x;
dfs1(y,x);
sizes[x]+=sizes[y];
if(!son[x]||sizes[y]>sizes[son[x]]) son[x]=y;
}
}
void dfs2(int x,int topfather){
dfn[x]=++dfstime;//dfs序
top[x]=topfather;//这个点所在重链的顶端,对于求lca和链有极大帮助
if(!son[x]) return;
dfs2(son[x],topfather);//我们首先进入重儿子来保证一条重链上各个节点dfs序连续
for(int i=head[x];i;i=nxt[i]){
int y=ver[i];
if(y!=son[x]&&y!=fa[x]) dfs2(y,y);//位于轻链底端,top为本身
}
}
int lca(int x,int y){
while(top[x]!=top[y]){
if(dep[top[x]]<dep[top[y]]) swap(x,y);
x=fa[top[x]];
}
if(dep[x]>dep[y]) swap(x,y);
return x;
}
void update(int x,int val){
for(;x<=dfstime;x+=lowbit(x)) treesum[x]+=val;
}
int query(int x){
int res=0;
for(;x;x-=lowbit(x)) res+=treesum[x]; return res;
}
bool cmp(int x,int y){return dfn[x]<dfn[y];}
inline void solve1(){
int x=0,tp=0;
for(int i=0;str[i];i++){
x=c[x][str[i]-'a']; a[++tp]=x;//在 trie 树中统计走过的节点
}
sort(a+1,a+1+tp,cmp);
bool flag=false;
for(int i=1;i<=tp;i++){
update(dfn[a[i]],1);//相邻两个节点在树上的位置 +1 ,表示多一个串匹配
if(flag) update(dfn[lca(a[i],a[i-1])],-1);
else flag=true;
}
}
int main(){
cin>>n;
for(int i=1;i<=n;i++) scanf("%s",str),ins(str,i);
build();
// for(int i=1;i<=cnt;i++) cout<<fail[i]<<" "<<i<<endl;
for(int i=1;i<=cnt;i++) add(fail[i],i);
dfs1(0,cnt+1); dfs2(0,0);
// cout<<dfstime<<endl;
// for(int i=0;i<=cnt;i++) cout<<dfn[i]<<" "; cout<<endl;
cin>>Q;
while(Q--){ int opt;
scanf("%d",&opt);
if(opt==1) scanf("%s",str),solve1();
else if(opt==2){
int x; scanf("%d",&x);
printf("%d\n",query(dfn[val[x]]+sizes[val[x]]-1)-query(dfn[val[x]]-1));
}
}
system("pause");
return 0;
}