非常好的题
sol.1
每次查询的时候暴力枚举起始点,然后一位一位比较,复杂度
O
(
m
∣
s
∣
2
)
O(m|s|^2)
O(m∣s∣2)
期望得分
20
p
t
s
20pts
20pts
sol.2
进行优化,我们可以每次暴力跑一遍
k
m
p
kmp
kmp,复杂度
O
(
m
∣
s
∣
)
O(m|s|)
O(m∣s∣)
期望得分
40
p
t
s
40pts
40pts
sol.3
我们发现有多个模式串,考虑对每一个串建立一棵AC自动机
然后把问题简化一下,我们考虑单次询问
也就是说,在AC自动机上,如何快速的求出
t
t
t在
s
s
s中出现的次数
显然我们发现就是找到
t
t
t的结尾节点,看
s
s
s中有几个点的
f
a
i
l
fail
fail的
f
a
i
l
fail
fail的
⋯
\cdots
⋯的
f
a
i
l
fail
fail是
t
t
t结尾节点就可以
所以我们可以枚举
s
s
s,每次往上找
f
a
i
l
fail
fail
复杂度可以卡到
O
(
m
∣
s
∣
2
)
O(m|s|^2)
O(m∣s∣2)
期望得分
20
p
t
s
20pts
20pts
sol.4
考虑AC自动机的一个性质
通常来说我们说AC自动机=trie树+kmp
但是我们观察
f
a
i
l
fail
fail指针,有一个非常有趣的现象就是每个点的
f
a
i
l
fail
fail指针指向的点是唯一的
也就是说
f
a
i
l
fail
fail指针的反向构成的也是一个树形结构,我们叫他失配树
说明AC自动机还等于trie树+失配树
所以我们可以建立失配树,然后每次查找的时候找这个节点的失配树上的子树中有多少个另一个里面的
复杂度同样可以卡到
O
(
m
∣
s
∣
2
)
O(m|s|^2)
O(m∣s∣2)
期望得分
20
p
t
s
20pts
20pts
sol.5
我们考虑如何快速的求一个点的子树中有多少个被标记的节点
我们可以求出失配树上的dfs序,这样一个子树中的编号就是连续的了,我们就可以用树状数组来进行维护
所以查询的时候就是先把
s
s
s上的每个点标记一下,然后
t
t
t的子树中求一下树状数组就可以
复杂度
O
(
m
∣
s
∣
log
n
)
O(m|s|\log n)
O(m∣s∣logn)
期望得分
40
p
t
s
40pts
40pts
sol.6
考虑进一步的优化,我们发现我们的复杂度的瓶颈在于
∣
s
∣
|s|
∣s∣,每次都标记一遍耗费了大量的复杂度
我们发现,AC自动机的根到一个点能够确定唯一的字符串,所以我们尝试在AC自动机上进行一次dfs,遍历每一个点
那么为了快速的计算,我们应该把询问离线
在遍历的过程中,我们把从根到该点的路径上的所有点进行标记
标记完了之后,我们计算所有和该点有关的询问的答案,也就是在失配树上进行查询
然后访问他的子树
访问完之后,撤销掉该点的贡献
复杂度
O
(
n
log
n
)
O(n\log n)
O(nlogn)
期望得分
100
p
t
s
100pts
100pts
tips.1
Q:如何建立AC自动机?
A:显然暴力建立是不行的,最差需要
O
(
n
2
)
O(n^2)
O(n2)的复杂度,但是我们发现我们每次的前缀通常情况下是相同的,所以在插入的时候我们不需要每次从根遍历,利用一个栈模拟一下,直接从中间往下走就可以了,这样保证复杂度在
O
(
n
)
O(n)
O(n)级别
tips.2
Q:算法流程?
A:
1、读入,stack模拟建出Trie
2、在Trie上建AC自动机
3、将失败指针的指向关系(子到父,除根指向根的),按父子关系存成一棵失配树
4、求失配树的dfs序,为dfs序配上树状数组
5、将所有询问<S中有多少个T>,挂到S在Trie对应的结点上
6、dfs遍历整棵Trie
6-1、新访问结点cur时,将cur在失配树dfs序的对应位置上+1
6-2、计算所有与cur有关的询问(cur对应的S中有多少T1、T2……)
——对于每个询问,求失配树dfs序上的相应区间和
6-3、处理cur的Trie子树
6-4、回溯前,将cur在失配树dfs序对应位置-1
7、输出所有答案
tips.3
这里同时又两棵树,trie和失配树,有的时候需要用trie,有的时候用失配树
在最后的dfs的时候,我们需要在trie上进行dfs,所以建立AC自动机的时候,不能进行记忆化(当然多开一个数组也行),要按照最原始的AC自动机来,可以证明这样建树的时候复杂度均摊后有保证的