题解
学习来源1 · 大佬博客直通车
学习来源2 · 大佬博客直通车
学习来源3 · 大佬博客直通车
在ac自动机里,如果字符串a可以通过fail指针指向字符串b,那么就说明a串中包含b串
问y串中有多少个x串,等同于问:
y中有多少个节点的fail指针直接或间接指向x的末尾节点
可以看到主要是根据fail进行跳转,
建立ac自动机后,以fail的角度看,其构造形似是一颗树,建立起以fail为关系的树,现在我们把它就做fail树,其性质有:
一个字符串的后缀中有一个字符串等价fail树中一个节点的子树中有另一个节点,也就是看一个点在Trie中到根节点的路径上有多少个属于那个点的Fail树。
再看一遍题目,现在问题等价于:
统计x的子树中来自y的节点的个数,
而实际的答案就是 fail树上x串末尾点的子树与Trie树上x到y的树链的交集中点的个数
由于在一颗树中,一个节点及其子树在DFS序中是连续的一段,那么我们可以用一个树状数组来维护答案
所以,大致步骤如下:
根据ac自动机建立fail树,
dfs求出每个串的起始位置和末尾位置,这将用于树状数组区间求和
保存每个问题对应的串和编号,
再跑一遍trie树,当遍历到代表y串的节点时,查询tire树中 root - y 的路径上,有多少个节点属于fail树中x的子树
我终于会了…
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int, int> pii;
const int N = 1e6 + 10;
const int INF = 0x3f3f3f3f;
int n, m;
int pat[N], k = 0;
namespace AC {//AC自动机板子
const int maxn = 26;
struct Tree {//字典树
int fail;//失配指针
int vis[maxn];//子节点位置
int end;//标记有几个单词以这个节点
int father;
} AC[N];
int cnt = 0;//编号
vector<int> Trie[N];//再次构建tire树 用于后面遍历trie树
void clear(int x) {//清空
memset(AC[x].vis, 0, sizeof(AC[x].vis));
AC[x].fail = 0;//结束标志 一般以0为root
AC[x].end = 0;
}
void build(string s) {
int len = s.length();
int now = 0;//字典树当前指针
for (int i = 0; i < len; ++i) {
if (s[i] == 'B') {
now = AC[now].father;
} else if (s[i] == 'P') {
pat[++k] = now;
} else {
if (AC[now].vis[s[i] - 'a'] == 0) {
//trie树没有这个子节点
AC[now].vis[s[i] - 'a'] = ++cnt;
clear(cnt);
AC[cnt].father = now;
Trie[now].push_back(cnt);
}
now = AC[now].vis[s[i] - 'a'];
}
}
}
queue<int> q;
void get_fail() {//构造fail指针
for (int i = 0; i < maxn; ++i) {//特殊处理root
if (AC[0].vis[i]) {
AC[AC[0].vis[i]].fail = 0;
q.push(AC[0].vis[i]);
}
}
while (!q.empty()) {
int u = q.front();
q.pop();
for (int i = 0; i < maxn; ++i) {
if (AC[u].vis[i]) {//存在子节点
AC[AC[u].vis[i]].fail = AC[AC[u].fail].vis[i];
//子节点的fail指针 指向父节点fail指针所指向的节点 的相同的子节点 vis[i]相同
//如果那个子节点不存在 相当于该子节点fail=0 指向了根节点
q.push(AC[u].vis[i]);
} else {//不存在子节点
AC[u].vis[i] = AC[AC[u].fail].vis[i];
//当前节点的子节点 指向 当前节点fail指针指向的节点的子节点
//把当前节点fail指向的节点 的子节点 作为自己的子节点
}
}
}
}
int beg[N], end[N], dfn = 0;
vector<int> Fail[N];//fail树
void dfs(int u) {//时间戳
beg[u] = ++dfn;
for (int v:Fail[u])
dfs(v);
end[u] = ++dfn;
}
void fail_tree() {//建fail树 目的是为了可以用dfs求出时间戳
for (int i = 1; i <= cnt; ++i) {
Fail[AC[i].fail].push_back(i);
}
dfs(0);//root
}
void init() {
cnt = 0;
clear(0);
}
}
using namespace AC;
namespace BIT {//树状数组板子
int bit[N];
int lowbit(int x) { return x & (-x); }
void add(int x, int val) {
for (; x <= N; x += lowbit(x)) {
bit[x] += val;
}
}
int ask(int x) {
int res = 0;
for (; x; x -= lowbit(x)) {
res += bit[x];
}
return res;
}
}
using namespace BIT;
string s;
vector<pii> query[N];//存问题及编号
int ans[N];//统计答案
void solve(int u) {
add(AC::beg[u], 1);//开始探索子树时 +1
for (auto p:query[u]) {
int id = p.second;//问题编号
int x = p.first;
//查询 tire树中 root-y的路径上 有多少个节点 属于fail树中x的子树
ans[id] = ask(AC::end[x]) - ask(AC::beg[x] - 1);
}
/*for (int i = 0; i < AC::maxn; ++i) {
int v = AC::AC[u].vis[i];
if (v) {
solve(v);
}
} //这样写是错误的 求fail指针的时候 如果没有儿子节点会造成各种诡异的循环*/
for (auto v:Trie[u])
solve(v);
add(AC::beg[u], -1);//结束时-1
}
int main() {
ios::sync_with_stdio(0);
init();
cin >> s;
build(s);//建树
get_fail();//构造fail指针
fail_tree();//建fail树
cin >> m;
for (int i = 1, x, y; i <= m; ++i) {
cin >> x >> y;
query[pat[y]].push_back({pat[x], i});
}
solve(0);//从根节点跑一遍trie树统计答案
for (int i = 1; i <= m; ++i) {
cout << ans[i] << endl;
}
return 0;
}
来源:CSDN
作者:Zaller
链接:https://blog.csdn.net/Yubing792289314/article/details/104558884