题意简述
给 (n) 个字符串, 多次询问第 (x) 串在第 (y) 串中的出现次数.
题目分析
首先当然要打开题目. 然后看题.
不难往字符串这方面想. 而多串匹配自然想到了建AC自动机.
怎么建图
很简单, 在 (Trie) 上遇到 ("B") 就返回父亲结点, 遇到 ("P") 就把当前结点标记为结束结点. 遇到小写字符往儿子走就行.
从上面发现实现时还要记录每个点的父亲.
转换模型
考虑什么时候 (x) 串会是 (y) 串的子串. 实际上就是存在一个结点 (u) 在 (Root) 结点到 (y) 串的结束路径上, 满足其跳 (Fail) 时会经过 (x) 串的结束结点.
这时你可能还是一脸懵逼. 没事, 接着看下去吧!
几个结论(性质)
- 对于一个字符串 (u) , 从 (Root) 结点到其结束结点所走过的路径都是其前缀.
- 一个结点 (v) 的 (Fail) 及其 (Fail) 的 (Fail) 都是 (v) 的后缀.
- 一个字符串 (u) 的前缀的后缀即是其子串.
- (Fail) 关系是一棵树.
思考结论
现在可能有点头绪了. 继续看吧.
(x) 串是 (y) 串的子串
(<=>) (x) 串是 (y) 串前缀的后缀
(<=>) (x) 串的结束结点会被 (Root) 结点到 (y) 串结束结点路径上的某个点跳 (Fail) 跳到.
现在好像有点意思了.
但题目要求的是出现次数, 如果对于每组询问都暴力跳 (y) 串每一个前缀的 (Fail) 的话时间恐怕要爆炸.
换一种思考问题的角度:
如果把 (Fail) 反向的话, 是不是就变成了 (x) 串逆向跳 (Fail) 能跳到 (Root) 结点到 (y) 串结束结点路径上多少个点?
并且貌似现在只要考虑 (Fail) 的关系...再联系一下上文的结论 (4.) ...
可以把 (Fail) 提出来单独建树! 问题就变成了多次询问 (x) 的子树中包含 (Root) 结点到 (y) 结点路径上点的数目. 这个问题可以用 树上 (DFS) 序 转化为多次单点修改+区间查询.
但是如果每次询问都独立处理恐怕还是有点慢...
不急, 回想一下打字机工作的流程, ("B") 操作回到父亲, ("P") 操作获取新字符串, 小写字符则进入儿子. 好像 (DFS) 的过程啊... 而且每一个编号相邻字符串在 (DFS) 上的确定顺序也是连续的?
到这里, 发现可以将询问离线然后按照 (y) 串编号升序排序, 然后直接模拟打字机的工作, 在 (Trie) 上 (DFS), 维护一个 (BIT), 进入儿子就 (Add), 退出就(Del), 遇到结束结点就处理以其为 (y) 串的询问.
至此, 本题完美解决.
代码实现
#include <cstdio>
#include <queue>
#include <cstring>
#include <algorithm>
const int N = 1e5;
int n, nL;
char S[N + 5];
int m, Ans[N + 5];
struct Query {int x, y, id;} Q[N + 5];
int Head[N + 5], ECnt;
struct Edge {int nxt, to;} E[N + 5];
int Size[N + 5], Id[N + 5], ICnt;
void Add (int, int);
bool Cmp (Query, Query);
void DFS (int);
struct ACAM {
int Endpos[N + 5];
int Fa[N + 5], Cnt, Root;
int Trans[N + 5][26 + 5], Fail[N + 5];
void clear() {
Cnt = Root = 1;
for (int i = 0; i < 26; ++i) Trans[0][i] = Root;
} void insert (char * Str) {
int tap, RCnt;
RCnt = 0, tap = Root, nL = strlen (Str);
for (int i = 0; i < nL; ++i) {
char tbp = Str[i];
if (tbp == 'B') tap = Fa[tap];
else if (tbp == 'P') Endpos[++RCnt] = tap;
else {
int &tcp = Trans[tap][tbp - 'a'];
if (!tcp) tcp = ++Cnt, Fa[tcp] = tap;
tap = tcp;
}
}
} void build () {
std::queue <int> Q;
for (int i = 0; i < 26; ++i) Trans[0][i] = Root;
Q.push (Root);
while (Q.size()) {
int tap = Q.front(); Q.pop();
for (int i = 0; i < 26; ++i) {
int &tbp = Trans[tap][i];
if (tbp) {
Q.push (tbp), Fail[tbp] = Trans[Fail[tap]][i];
Add (Trans[Fail[tap]][i], tbp);
} else tbp = Trans[Fail[tap]][i];
}
}
}
} Zoe;
struct BIT {
int c[N + 5];
int lowbit (int p) {return (p & (-p));
} void add (int p, int k) {
for (int i = p; i <= nL; i += lowbit (i)) c[i] += k;
} int query (int p) {
int tap = 0;
for (int i = p; i; i -= lowbit (i)) tap += c[i];
return tap;
}
} Nico;
int main() {
scanf ("%s%d", S, &m);
Zoe.clear(), Zoe.insert (S), Zoe.build(), DFS(Zoe.Root);
for (int i = 1; i <= m; ++i) Q[i].id = i, scanf ("%d%d", &Q[i].x, &Q[i].y);
std::sort (Q + 1, Q + 1 + m, Cmp);
int Now, p, Tot;
Now = Zoe.Root, Tot = 0, p = 1;
for (int i = 0; i < nL; ++i) {
char tap = S[i];
if (tap == 'P') {
++Tot;
while (p <= m && Q[p].y < Tot) ++p;
while (p <= m && Q[p].y == Tot) {
int tbp = Zoe.Endpos[Q[p].x];
Ans[Q[p].id] = Nico.query (Id[tbp] + Size[tbp] - 1) - Nico.query (Id[tbp] - 1);
++p;
}
} else if (tap == 'B') Nico.add (Id[Now], -1), Now = Zoe.Fa[Now];
else Now = Zoe.Trans[Now][tap - 'a'], Nico.add (Id[Now], 1);
} for (int i = 1; i <= m; ++i) printf ("%d
", Ans[i]);
return 0;
} void Add (int u, int v) {
E[++ECnt] = (Edge) {Head[u], v}, Head[u] = ECnt;
} bool Cmp (Query X, Query Y) {
return X.y < Y.y;
} void DFS (int p) {
Id[p] = ++ICnt, Size[p] = 1;
for (int i = Head[p]; i; i = E[i].nxt)
DFS (E[i].to), Size[p] += Size[E[i].to];
}