哈哈,哈哈哈,哈哈哈哈 前前后后学了三天才彻底搞明白这题到底咋做,我是真的服了我自己了,菜的一
首先感谢一下luogu@yybyyb 大佬的题解,一步一步让我搞懂了AC自动机的原理。 其实基本的思路这篇题解已经写的非常详细了,这里就我自己的理解加一些方便理解的图片,同时也帮我自己消化消化这个题目。
首先有几个预备知识:
1.Tire(字典树)
这里放上百度百科,还是比较基础的数据结构。
2.KMP算法和AC自动机
这里不做太多说明,这里其实AC自动机也只是用到了一个模板,自行百度。
3.fail树
首先我们知道,如果当前节点为x,那么fail[x]即为当前节点在Tire树内最长的后缀字符串
那么反推,既然fail[x]所代表的字符串是x所代表的字符串的一个最长后缀字符串,所以我们得出结论,fail[x]所代表的字符串必定是x的一个子串(1)
随便举个例子,这里的fail[x]代表的字符串(bc),是x代表的字符串(abc)的一个子串。
让我们先忘记上面的部分 给出定义,当我们剔去所有原来的边,只留下fail组成的边,我们就会得到一棵fail树。(2)
那fail树长到底什么样子捏? 就拿题目举例子,如下。
好,那么我们既然知道了fail节点指向的字符串是原串当中的一个子串(1),以及fail树(2),那我们即可得出一个结论,即为
设{ y| y ∈ 1....n }为fail[x]所代表的子串,对于节点x,y即为所有其他串在x串中的子串。 对于某个特定的y[i],我们可以知道,x的fail访问到了y[i]几次,就是,y[i]出现在x中的次数。
是不是觉得绕的一B,来画个图稍微解释一下
在里ababa里面,ba出现了两次,所以ababa的fail数组访问到了ba也就是8号点。
综上所述,我们可以知道,若要串x在串y中出现了几次,只需要将y的所有节点全部标1,然后找到能访问了多少次x节点,即为单次查询的答案,但很显然,单次查询的复杂度为O(m * n),所以考虑离线查询,按照y节点来查询。
那么大概思路有了,我们要怎么实现查询呢,这是一个大问题?
首先,我们要将fail树的所有边翻转一下,如下图
然后我们就可以观察到,如果我有一个串x,那么x如果指向某个节点y,x就一定包含在y内。
所以问题就转化为,找到某个串x往下搜索,这些搜到的串y必定包含x;那不就简单了,直接dfs过去就完事了。标记一个low数组和dfn数组,即为某一个串的所有子树能搜到的节点。
我们再用树状数组(建议去看一下树状数组)维护一下,然后我们用树状数组修改单点的值,那么我们就能直接知道,如果我访问到一个串,这个串包含了哪些子串以及这些子串都出现了几次。
好了直接上代码
// last try
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;
int tot, nd[N], n, dep, h[N];
int low[N], c[N], dfn[N], teg = 1;
int l[N], r[N], ans[N];
struct node {
int fail, vis[26], Vis[26];
int fa, lt = 0;
}p[N];
struct Edge {
int ed, next;
}e[N>>1];
struct Question {
int x, y;
int id, res;
}q[N];
int lowbit ( int x ) {
return x&(-x);
}
void Modify ( int x, int w ) {
while( x <= dep ){
c[x] += w;
x += lowbit(x);
}
}
int getsum ( int x ) {
int ret = 0;
while ( x ) {
ret += c[x];
x -= lowbit(x);
}
return ret;
}
bool comp ( Question q1, Question q2 ) {
return q1.y < q2.y;
}
void add( int u, int v ) {
e[teg].ed = v;
e[teg].next = h[u];
h[u] = teg ++;
}
void build () {
queue <int> Q;
for ( int i = 0; i < 26; i ++ ) {
if ( p[0].vis[i] )
Q.push( p[0].vis[i] );
}
while ( !Q.empty() ) {
int u = Q.front();
Q.pop();
for ( int i = 0; i < 26; i ++ ) {
int tu = p[u].vis[i];
if ( tu ) {
p[tu].fail = p[p[u].fail].vis[i];
Q.push(tu);
}
else {
p[u].vis[i] = p[p[u].fail].vis[i];
}
}
}
}
void dfs ( int u ) {
dfn[u] = ++ dep;
for ( int i = h[u]; i; i = e[i].next ) {
dfs( e[i].ed );
}
low[u] = dep;
}
void Dfs ( int u ) {
Modify( dfn[u], 1 );
if ( p[u].lt ) {
int ty = p[u].lt;
for ( int i = l[ty]; i <= r[ty]; i ++ ) {
int tx = nd[q[i].x];
q[i].res = getsum( low[tx] ) - getsum( dfn[tx] - 1 );
}
}
for ( int i = 0; i < 26; i ++ ) {
if ( p[u].Vis[i] ) {
Dfs( p[u].Vis[i] );
}
}
Modify( dfn[u], -1 );
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
string ss; cin >> ss;
memset( r, -1, sizeof r );
int now = 0;
for ( int i = 0; i < ss.length(); i ++ ) {
if ( ss[i] <= 'z' && ss[i] >= 'a' ) {
int val = ss[i] - 'a';
if ( !p[now].vis[val] ) {
p[now].vis[val] = ++tot;
p[tot].fa = now;
}
now = p[now].vis[val];
}
if ( ss[i] == 'B' ) now = p[now].fa;
if ( ss[i] == 'P' ) {
p[now].lt = ++n;
nd[n] = now;
}
}
for ( int i = 0; i <= tot; i ++ ) {
for( int j = 0; j < 26; j ++ ) {
p[i].Vis[j] = p[i].vis[j];
}
}
build();
for ( int i = 1; i <= tot; i ++ ) {
add( p[i].fail, i );
}
dfs (0);
int Q; cin >> Q;
for ( int i = 1; i <= Q; i ++ ) {
cin >> q[i].x >> q[i].y;
q[i].id = i;
}
sort ( q + 1, q + 1 + Q, comp );
for ( int i = 1; i <= Q; i ++ ) {
int z = q[i].y;
if ( !l[z] ) l[z] = i;
r[z] = i;
}
Dfs( 0 );
for ( int i = 1; i <= Q; i ++ ) ans[q[i].id] = q[i].res;
for ( int i = 1; i <= Q; i ++ ) cout << ans[i] << endl;
}
结束,因为没学好树状数组,导致我一直看不懂题解,卡了两天才想明白... ... 这里面dfn和low数组直接找到串的子树也是比较巧妙,还是值得学习的