AC自动机 & Fail树 专题练习
Fail树就是AC自动机建出来的Fail指针构成的树。
【bzoj3172】【xsy1713】单词
题意
给定一些单词,求每个单词在所有单词里面的出现次数。
分析
构建Fail树,记录每个单词最后一个位置在Fail树上的位置。
每个单词访问的每个节点的\(has+1\)。
然后在Fail树上进行树形dp。
每次询问,求对应位置的dp值。
核心代码
int n;
char s[L];
int rt,tot;
int loc[N];
struct Trie {
int nx[A];
LL has;
int fail;
}tr[S];
int q[S];
int qh,qt;
void Ins(int frm,int &now,char *s,int cur,int len) {
if (!now)
now=++tot;
tr[now].has++;
if (cur==len+1) {
loc[frm]=now;
return;
}
int go=s[cur]-'a';
Ins(frm,tr[now].nx[go],s,cur+1,len);
}
void Build(void) {
rep(i,0,A-1)
if (!tr[rt].nx[i])
tr[rt].nx[i]=rt;
else {
tr[tr[rt].nx[i]].fail=rt;
q[++qt]=tr[rt].nx[i];
}
while (qh!=qt) {
int now=q[++qh];
rep(i,0,A-1)
if (!tr[now].nx[i])
tr[now].nx[i]=tr[tr[now].fail].nx[i];
else {
tr[tr[now].nx[i]].fail=tr[tr[now].fail].nx[i];
q[++qt]=tr[now].nx[i];
}
}
per(i,qt,1)
tr[tr[q[i]].fail].has+=tr[q[i]].has;
}
int main(void) {
#ifndef ONLINE_JUDGE
freopen("xsy1713.in","r",stdin);
freopen("xsy1713.out","w",stdout);
#endif
rt=tot=1; scanf("%d",&n);
rep(i,1,n) {
scanf("%s",s+1);
Ins(i,rt,s,1,strlen(s+1));
}
Build();
rep(i,1,n) {
LL t=tr[loc[i]].has;
printf("%lld\n",t);
}
return 0;
}
【bzoj2754】【xsy1712】喵星球上的点名
题意
知道\(N\)位学生的姓、名。
\(M\)次点名,每次若点到某位学生姓或名的子串,该学生要报告。
问每次有多少学生报告,且最后每位学生报告多少次。
分析1
把每次点名的信息,即多个字符串,建立AC自动机。
对于每一位学生,考虑它对答案的贡献。
一种暴力的想法是:
把学生的姓和名分别在AC自动机上跑一遍,跑到一个位置的时候沿着Fail指针往上跳暴力更新答案,用时间戳维护某位学生是否在某次中被点名。
由于随机生成的树的深度不会特别大。
所以这种做法的效率虽然是\(O(n^2)\),但是实际跑起来会很快。
这里有很多小小的细节。
由于这个字符没有什么限制,所以考虑使用map来连边,然后遍历的时候还要用map<int,int> iterator
。
由于一个终点位置可能有多个字符串,所以要使用vector来记录。
核心代码:
//...
#define rep(i,a,b) for (int i=(a);i<=(b);i++)
#define pb push_back
typedef map<int,int> MI;
typedef map<int,int>::iterator MII;
typedef vector<int> VI;
//...
int rd(void);
int n,m;
struct Name {
int len; VI lis;
void Read(void) {
len=rd();
rep(i,1,len) {
int x=rd();
lis.pb(x);
}
}
}fam[N],uni[N],men[M];
int q[S];
int qh,qt;
int rt,tot;
struct T {
int fail; MI nx;
VI edp;
}tr[S];
int cnt[N];
int vis[M],eff[M];
//...
void Init(void) {
rt=tot=1;
tr[rt].fail=rt;
}
void Ins(int frm,int &now,Name nam,int cur) {
if (!now)
now=++tot;
if (cur==nam.len+1) {
tr[now].edp.pb(frm);
return;
}
int go=nam.lis[cur-1];
Ins(frm,tr[now].nx[go],nam,cur+1);
}
void Build(void) {
q[++qt]=rt;
while (qh!=qt) {
int now=q[++qh];
for (MII it=tr[now].nx.begin();it!=tr[now].nx.end();it++) {
int go=it->first,nxp=it->second; int t=tr[now].fail;
while (t!=rt&&!tr[t].nx.count(go))
t=tr[t].fail;
if (tr[t].nx.count(go)&&now!=rt)
t=tr[t].nx[go];
tr[nxp].fail=t;
q[++qt]=nxp;
}
}
}
int Trans(int now,int go) {
int t=now;
while (t!=rt&&!tr[t].nx.count(go))
t=tr[t].fail;
if (tr[t].nx.count(go))
t=tr[t].nx[go];
return t;
}
void Update(int frm,int now) {
int t=now;
while (t!=rt) {
rep(i,1,tr[t].edp.size()) {
int to=tr[t].edp[i-1];
if (vis[to]!=frm) {
vis[to]=frm;
eff[to]++;
cnt[frm]++;
}
}
t=tr[t].fail;
}
}
void Traver(int frm,Name nam) {
int now=rt;
rep(i,1,nam.len) {
int go=nam.lis[i-1];
now=Trans(now,go);
Update(frm,now);
}
}
int main(void) {
//...
n=rd(),m=rd();
rep(i,1,n) {
fam[i].Read();
uni[i].Read();
}
rep(i,1,m)
men[i].Read();
Init();
rep(i,1,m)
Ins(i,rt,men[i],1);
Build();
rep(i,1,n) {
Traver(i,fam[i]);
Traver(i,uni[i]);
}
//...
}
分析2
上述算法的问题的复杂度瓶颈出现在:
跑到一个位置的时候沿着Fail指针暴力更新答案
这一步随机下来虽然很快,但是它是\(O(n)\)的。
我们考虑对它进行优化。
对于每一个学生,它访问的节点,是Fail树上多条到根的路径的并。
考虑两个询问。
【询问2】
求经过一个点的路径的并的个数
我们只需要把路径的并上的每一个点的点权+1即可。
实现方法(粘自popoqqq的博客)
http://blog.csdn.net/PoPoQQQ/article/details/43020531
将所有节点按照DFS序排序
每个点到根的路径上的所有节点权值+1
相邻两个点的LCA到根的路径上的所有节点权值-1
即是树链的并
当然我们不必修改路径查询单点 只需对单点进行修改 然后查询子树 用树状数组维护DFS序即可
然而这道题并不用动态更改。
所以前缀标记一下就好了。
【询问1】求一个路径的并经过的点的点权之和
把每个作为字符串结尾的点的点权赋为1。
在树上预处理一下前缀和。
每次树上倍增,并用前缀和快速搞出来就好了。
分析3
前两种方法都是对点名的信息建立AC自动机。
但实际上,对姓、名建立AC自动机亦可以。
首先,根据题目的限制条件。
我们可以把姓和名用一个连字符连接起来,当做一个字符串来处理。
对姓名建立AC自动机,标记终点。
然后顺序处理每一个点名信息。
【询问1】首先把该点名信息在AC自动机上跑到终点。
若跑不到终点就跑不了了,那么答案就是0。
若跑到了终点,那么答案就是该点在Fail树上对应的子树中,有多少个点被标记。
这个东西提前进行树形dp即可。
【询问2】把点名信息放在Fail树上。
Fail树自顶向下遍历,统计每个点到根的路径中被标记的次数。
小结
(1)多个字符串的处理手段
对于多个字符串的匹配等问题,常考虑能否把多个字符串通过中间加连字符的方式合并为一个字符串,化简问题。
这种思考的东西通常是逐步满足的思考。
(2)树链的并
一些初步的认知:
求dfn序。相邻两个求LCA进行转化。
(3)树上的暴力算法
树上有种随机起来效率不错的暴力算法:树上每个点往根跳。
这很多时候可以骗分。
这基于随机情况下,树上的点的深度不会特别大。
(4)关于map
typedef map<int,int> MI;
typedef map<int,int>::iterator MII;
MII mp;
void Traverse(void) {
for (MII it=mp.begin();it!=mp.end();it++) {
int x=it->first,y=it->second;
//...
}
}
【bzoj3881】【xsy1714】Divljak
题意
Alice有n个字符串S_1,S_2...S_n,Bob有一个字符串集合T,一开始集合是空的。
接下来会发生q个操作,操作有两种形式:
“1 P”,Bob往自己的集合里添加了一个字符串P。
“2 x”,Alice询问Bob,集合T中有多少个字符串包含串S_x。(我们称串A包含串B,当且仅当B是A的子串)
Bob遇到了困难,需要你的帮助。
分析
分析1
把\(S_1,S_2,...,S_n\)建立AC自动机。
那么,对于每个操作1,相当于对到根的树链的并上的每一个点增值;对于操作2,相当于求单点的值。
再次转化:单点修改,子树求和。
BIT+dfn序维护即可。
核心代码
//...
int n;
char s[L];
int rt,tot; int loc[N];
struct T {
int nx[A];
int fail;
}tr[S];
int q[S]; int qh,qt;
struct G {
int v,nx;
G(int _v=0,int _nx=0) {
v=_v,nx=_nx;
}
}mp[E];
int totG,hd[S];
int dep[S]; int unit,pre[U][S];
int siz[S]; int dfn[S],num;
int m;
char p[L];
int lis[L];
int trA[S];
void InsTrie(int frm,char *s,int len) {
int now=rt;
rep(i,1,len) {
int go=s[i]-'a';
if (!tr[now].nx[go])
tr[now].nx[go]=++tot;
now=tr[now].nx[go];
}
loc[frm]=now;
}
void Build(void) {
rep(i,0,A-1)
if (!tr[rt].nx[i])
tr[rt].nx[i]=rt;
else {
tr[tr[rt].nx[i]].fail=rt;
q[++qt]=tr[rt].nx[i];
}
while (qh!=qt) {
int now=q[++qh];
rep(i,0,A-1)
if (!tr[now].nx[i])
tr[now].nx[i]=tr[tr[now].fail].nx[i];
else {
tr[tr[now].nx[i]].fail=tr[tr[now].fail].nx[i];
q[++qt]=tr[now].nx[i];
}
}
}
void Ins(int u,int v) {
mp[++totG]=G(v,hd[u]); hd[u]=totG;
}
void DFS(int now) {
siz[now]=1; dfn[now]=++num;
fore(k,now) {
dep[mp[k].v]=dep[now]+1; pre[0][mp[k].v]=now;
DFS(mp[k].v);
siz[now]+=siz[mp[k].v];
}
}
int lowbit(int i) {
return i&-i;
}
void Add(int x,int ad) {
for (int i=x;i<=tot;i+=lowbit(i))
trA[i]+=ad;
}
int Query(int x) {
int sum=0;
for (int i=x;i>0;i-=lowbit(i))
sum+=trA[i];
return sum;
}
int LCA(int x,int y) {
if (dep[x]<dep[y]) swap(x,y);
per(i,unit,0)
if (dep[x]-(1<<i)>=dep[y])
x=pre[i][x];
if (x==y) return x;
per(i,unit,0)
if (pre[i][x]!=pre[i][y])
x=pre[i][x],y=pre[i][y];
return pre[0][x];
}
void Traver(char *p,int len) {
int now=rt;
rep(i,1,len) {
int go=p[i]-'a';
now=tr[now].nx[go];
lis[++lis[0]]=now;
}
}
int cmp(int a,int b) {
return dfn[a]<dfn[b];
}
void Modify(char *p,int len) {
lis[0]=0;
Traver(p,len);
sort(lis+1,lis+lis[0]+1,cmp);
// int t;
lis[0]=unique(lis+1,lis+lis[0]+1)-lis-1;
rep(i,1,lis[0]) {
Add(dfn[lis[i]],1);
// t=Query(dfn[2]+siz[2]-1)-Query(dfn[2]-1);
}
rep(i,1,lis[0]-1) {
int anc=LCA(lis[i],lis[i+1]);
Add(dfn[anc],-1);
// t=Query(dfn[2]+siz[2]-1)-Query(dfn[2]-1);
}
}
int QueryAns(int x) {
int id=loc[x];
int rl=Query(dfn[id]-1);
int rr=Query(dfn[id]+siz[id]-1);
int t=rr-rl;
return t;
}
//...
int main(void) {
#ifndef ONLINE_JUDGE
freopen("xsy1714.in","r",stdin);
freopen("xsy1714.out","w",stdout);
#endif
n=rd(); rt=tot=1;
rep(i,1,n) {
scanf("%s",s+1);
InsTrie(i,s,strlen(s+1));
}
Build();
rep(i,2,tot)
Ins(tr[i].fail,i);
unit=(int)(log(tot)/log(2)); pre[0][rt]=rt;
DFS(rt);
rep(i,1,unit) rep(j,1,tot)
pre[i][j]=pre[i-1][pre[i-1][j]];
//...
}
分析2
首先,我们考虑对P建立AC自动机。
离线建的功能肯定更强,所以我们离线所有操作,建立P的AC自动机。
考虑询问2,我们这样来描述:求AC自动机上某一个点在Fail树上的子树中被多少个不同的P访问过。所以我们只需要处理出每个P访问了那些点,然后把树的问题用dfn序转化,再用HH的项链那道题的方法解决即可。
【bzoj2434】【xsy1715】阿狸的打字机
题意
粘贴自popoqqq的博客:http://blog.csdn.net/PoPoQQQ/article/details/41518097
初始字串为空,首先给定一系列操作序列,有三种操作:
1.在结尾加一个字符
2.在结尾删除一个字符
3.打印当前字串
然后多次询问第x个打印的字串在第y个打印的字串中出现了几次
分析
首先在线建立Trie树。
处理出Fail指针。
一个问题可以描述为\((x,y)\):
求\(\sum_{在AC自动机上,y的祖先k}[在Fail树上,x为k的祖先]\)
我们可以在Fail上树上倍增,枚举\(y\)的祖先\(k\),判断\(x\)是否为\(k\)的祖先。
现在考虑优化。
注意这是多组询问,我们可以考虑把问题离线下来。
由于我们要枚举\(y\)的祖先,所以考虑把Trie树进行DFS,并用树状数组动态更新单点,查询就只是子树求和了。
用dfn序映射到一条直线上。
小结
Fail树是AC自动机建立之后的附属品,也是建立之后使用AC自动机的必需品。
使用Fail树关键要抓住这样一些性质:
【Fail树的性质】
对于Fail树上的两个节点\((i,j)\),若\(i\)为\(j\)的祖先,那么有:
①到达AC自动机的\(i\)节点对应的字符串 是 到达AC自动机的\(j\)节点对应的字符串 的 子串
②到达AC自动机的\(j\)节点对应的字符串 包含着 到达AC自动机的\(i\)节点对应的字符串
建立AC自动机的对象不同,就要使用不同的性质进行处理。
AC自动机 & Fail树 专题练习的更多相关文章
- BZOJ 2434: [Noi2011]阿狸的打字机 [AC自动机 Fail树 树状数组 DFS序]
2434: [Noi2011]阿狸的打字机 Time Limit: 10 Sec Memory Limit: 256 MBSubmit: 2545 Solved: 1419[Submit][Sta ...
- BZOJ 3172: [Tjoi2013]单词 [AC自动机 Fail树]
3172: [Tjoi2013]单词 Time Limit: 10 Sec Memory Limit: 512 MBSubmit: 3198 Solved: 1532[Submit][Status ...
- 【BZOJ-3881】Divljak AC自动机fail树 + 树链剖分+ 树状数组 + DFS序
3881: [Coci2015]Divljak Time Limit: 20 Sec Memory Limit: 768 MBSubmit: 508 Solved: 158[Submit][Sta ...
- BZOJ2434 [Noi2011]阿狸的打字机(AC自动机 + fail树 + DFS序 + 线段树)
题目这么说的: 阿狸喜欢收藏各种稀奇古怪的东西,最近他淘到一台老式的打字机.打字机上只有28个按键,分别印有26个小写英文字母和'B'.'P'两个字母.经阿狸研究发现,这个打字机是这样工作的: 输入小 ...
- 【BZOJ-2434】阿狸的打字机 AC自动机 + Fail树 + DFS序 + 树状数组
2434: [Noi2011]阿狸的打字机 Time Limit: 10 Sec Memory Limit: 256 MBSubmit: 2022 Solved: 1158[Submit][Sta ...
- CF 163E. e-Government ac自动机+fail树+树状数组
E. e-Government 题目: 给出n个字符串,表示n个人名,有两种操作: ?string ,统计字符串string中出现的属于城市居民的次数. +id,把编号为id的人变为城市居民,如果已经 ...
- BZOJ2905: 背单词 AC自动机+fail树+线段树
$zjq$神犇一眼看出$AC$自动机 $Orz$ 直接就讲做法了 首先对每个串建出$AC$自动机 将$fail$树找到 然后求出$dfs$序 我们发现一个单词 $S_i$是$S_j$的子串当且仅当$S ...
- 【学习笔记】ac自动机&fail树
定义 解决文本串和多个模式串匹配的问题: 本质是由多个模式串形成的一个字典树,由tie的意义知道:trie上的每一个节点都是一个模式串的前缀: 在trie上加入fail边,一个节点fail边指向这个节 ...
- BZOJ2434 [Noi2011]阿狸的打字机 【AC自动机 + fail树 + 树状数组】
2434: [Noi2011]阿狸的打字机 Time Limit: 10 Sec Memory Limit: 256 MB Submit: 3610 Solved: 1960 [Submit][S ...
随机推荐
- 使用cookie保存页面登录信息
1.数据库连接配置页面:connectvars.php <?php//数据库的位置define('DB_HOST', 'localhost');//用户名define('DB_USER', 'r ...
- okhttp3教程(1)如何引入库
官网: https://github.com/square/okhttp https://github.com/square/okio 1,使用okhttp3需要两个库 在build.gradle c ...
- C语言中的const
今天探讨const,首先来说是将变量常量化.为什么要将变量常量化,原因有诸多好处有诸多.比如可以使数据更加安全不会被修改! 但是这个词有几个点要注意,那就是他究竟修饰了谁? 1.const int a ...
- 二、java中的基本数据类型
总结: 1.java中的基本数据类型有byte.short.int.long;float.double;char;boolean. 2.基本数据类型与1相对应分别占1.2.4.8;4.8;2;1.(单 ...
- 【leetcode❤python】326. Power of Three
#-*- coding: UTF-8 -*- class Solution(object): def isPowerOfThree(self, n): if n<=0: ...
- js九宫格的碰撞检测
说来惭愧,我一直以为四四方方的拖拽碰撞检测是一个比较容易的事情,后来试过一次,真是让我耗费了无数的脑细胞,原理其实不难,但是具体做起来可就让我很恶心,这可能跟我驾驭的代码数量有关系,我一般也就写半屏幕 ...
- Python 字符编码 zz
http://www.cnblogs.com/huxi/archive/2010/12/05/1897271.html 1. 字符编码简介 1.1. ASCII ASCII(American Stan ...
- MeshLab中进行点云配准
MeshLab是一个开源.可移植和可扩展的三维几何处理系统,主要用于交互处理和非结构化编辑三维三角形网格.它支持多种文件格式: import:PLY, STL, OFF, OBJ, 3DS, COLL ...
- awk 以HWI开头,并且:相邻两行的第一个字段完全相同;
## 思路:以HWI开头,并且:相邻两行的第一个字段完全相同:awk 'BEGIN{ last_col_1="xxxxxx"; last_row="bbbbbbbbbbb ...
- 安装64位版Oracle11gR2后无法启动SQLDeveloper的解决方案
安装64位版Oracle11gR2后发现启动SQL Developer时弹出配置java.exe的路径,找到Oracle自带java.exe后产生的路径“C:\app\用户名\product\11.2 ...