codeforces1207G Indie Album AC自动机-DFS-树状数组 算法日常[32/100]

题目链接

codeforces1207G Indie Album

题解

官方题解

  • 看到这里是多个字符串的匹配,我们可以用AC自动机(虽然我一开始想到是用很多后缀自动机(SAM),也有大佬实现了,在官方题解的评论区有),给要题目中要查询的字符集建立AC自动机
  • 并通过trie保持题目让我们构建的串
  • 通过dfs求出AC自动机的各节点的及其子节点的size大小,用于后面树状数组确定好匹配的区间
  • 最后再通过DFS让我们要匹配的串去AC自动机上进行跑动,通过树状数组来计数(菜鸡我第一次见这种sao操作)
  • 对于每个匹配完的DFS都要清除掉树状数组上的记录,以免对其他部分造成影响

AC代码

好像比赛时只有30个大佬过了这题,所以代码不是我这个小菜鸡写的,是借鉴了ASDFZ的一个oi大佬(我猜他是搞oi的)的代码

以及附上我对他代码的理解注释,我觉得这份代码写得真牛逼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
// 59470538	Aug/25/2019 23:34UTC+8	LittleBeetle	G - Indie Album	GNU C++11	Accepted	561 ms	89100 KB
// By LittleBeetle, contest: Educational Codeforces Round 71 (Rated for Div. 2), problem: (G) Indie Album, Accepted, #
#include<cstdio>
#include<vector>
using namespace std;
const int N=400002;
int n,q,i,j,k,opt,h[N],tt[N],v[N],ans[N];
char ch[3],s[N],ti[N];
vector<int>p[N],id[N];
void add(int a,int b){
// 看AC.dfs()发现,h[a]记录的应该是最后一个a指向的节点,而tt则记录的是同一层节点的前向路径,所以不是什么trie,而是一个奇特的最右儿子兄弟树!
// 因此可以AC.dfs()可以通过使用这样的for循环 `for(int j=H[i];j;j=to[j])` 访问整个一层的节点
tt[++k]=h[a];
h[a]=k;
// 这里v[k] 就是记录的是id节点的value, 用int 记录 char 是没有关系的
v[k]=b;
}
struct AC{
int cnt,t[N][26],fail[N],q[N],l,r,i,j,k;
int H[N],to[N],V[N],dfn[N],sz[N],Tim,c[N],loc[N];
int insert(char *s){
for(i=j=1;s[j];j++){
if(!t[i][s[j]-97])
t[i][s[j]-97]=++cnt;
i=t[i][s[j]-97];
}
return i;
}
void add(int a,int b){
to[++k]=H[a];
H[a]=k;
V[k]=b;
}
// 深度遍历右儿子以及右儿子的兄弟,统计size的值
void dfs(int i){
// dfn 是 各个节点被深度遍历的顺序的hash记录
dfn[i]=++Tim;
sz[i]=1;
for(int j=H[i];j;j=to[j])
dfs(V[j]),sz[i]+=sz[V[j]];
}
void Build(){
// bfs的l,r的写法
// 根节点用的是1
l=0;r=-1;
for(j=0;j<26;j++)
if(t[1][j])
fail[t[1][j]]=1,add(1,t[1][j]),q[++r]=t[1][j];
else
t[1][j]=1;
while(l<=r){
i=q[l++];
for(j=0;j<26;j++)
if(t[i][j])
fail[t[i][j]]=t[fail[i]][j],add(fail[t[i][j]],t[i][j]),q[++r]=t[i][j];
else
t[i][j]=t[fail[i]][j];
}
dfs(1);
}
// 这是一个反向树状数组,所以序数小的节点是大节点(父节点)
void Add(int x,int y){
while(x)
c[x]+=y,x-=x&-x;
}
int Query(int x){
int s=0;
while(x<=cnt)
s+=c[x],x+=x&-x;
return s;
}
void DFS(int i){
Add(dfn[loc[i]],1);
// 测试发现,i=0等一些没有到过的值的时候,是不会进入for的...
for(int j=0;j<p[i].size();j++)
ans[id[i][j]]=Query(dfn[p[i][j]])-Query(dfn[p[i][j]]+sz[p[i][j]]);
// 卡很久一个点,注意这里的for循环也是右儿子及其兄弟的遍历方式...===> 而且这里是要查询自动机的初始串的遍历!!!!!
for(int j=h[i];j;j=tt[j]){
// 这里是跑AC自动机的核心,失配之后 从i跑向j,然后再DFS
// 对的,跑自动机的时候是不用fail的,然后在这里跑下去,所以先算好了下面的,然后回溯算好了上面的
// 每一小块都要把对应的树状数组部分清除掉

// 2019年10月08日21:21:13 突然那么一瞬间,总算想清楚了多一点点
// 比如你dadada,然后AC自动机上只有模式串da,所以你虽然dfs向下走dadada,但是你每次添加的位置都是loc[v[i]],所以这里是一个比较松的耦合!
loc[v[j]]=t[loc[i]][s[v[j]]-97];
DFS(v[j]);
}
Add(dfn[loc[i]],-1);
}
void work(){
loc[0]=1;
DFS(0);
}
}T;
void init(){
T.cnt=1;
scanf("%d",&n);
for(i=1;i<=n;i++,j=0){
scanf("%d",&opt);
if(opt==2)
scanf("%d",&j);
add(j,i);
scanf("%s",ch);
s[i]=*ch;
}
scanf("%d",&q);
for(i=1;i<=q;i++){
scanf("%d%s",&j,ti+1);
p[j].push_back(T.insert(ti));
id[j].push_back(i);
}
}
int main(){
init();
T.Build();
T.work();
for(i=1;i<=q;i++)
printf("%d\n",ans[i]);
return 0;
}

背后故事

菜鸡我前前后后理解了这个题目3天…没想到自己被自己不太认识的树状数组卡了好久(丢人了…)–> 继续加油吧

每天一句叨叨

人生最美好的,就是在你停止生存时,也还能以你所创造的一切为人们服务。