【luogu P4770】[NOI2018] 你的名字(SAM)(线段树合并)
[NOI2018] 你的名字
题目链接:luogu P4770
题目大意
给你一个字符串 S,然后每次询问给你一个字符串 T 和 l,r。
要你求 T 中有多少个子串不是 S[l]~S[r] 组成的字符串的子串。
思路
首先我们看到很多的部分分是 \(l=1,r=|S|\)。
那我们先想想怎么搞这个,也就是求 T 中有多少个子串不是 S 的子串。
首先考虑反过来求有多少个串同时是 S,T 的子串。
然后我们考虑把 T 在 S 的 SAM 上面跑,然后每个位置有匹配长度 \(len\),然后对于 T 的 SAM 的每个点我们记录一个 \(pl_i\) 表示这个点是字符串哪个前缀跑出来的点,然后跑到这个位置的 \(len\) 就是能匹配的长度 \(ans_i\)。
然后答案就是枚举 \(T\) 的每个点(\(1\) 号点除外),贡献是 \(\max(0,len_i-\max(len_{fa_i},ans_{pl_i}))\)(这个 \(\max(0)\) 是防止 \(ans_pl_i\) 太大)
那复杂度为 \(O(|T|)\) 可以接受。
接着考虑 \(l,r\) 随便的情况。
那我们考虑会有什么问题,就是求这个 \(ans_i\) 的时候,你跑出来的点不一定行。
你要这个点是在 \(S_{l\sim r}\) 的 SAM 上才可以,亦或者说要这个点的 endpos 类里面有 \(l\sim r\) 的信息。
那要怎么判呢,我们考虑维护 S 的 SAM 中一个点的 endpos。
怎么维护呢,我们可以用动态开点线段树,然后父亲的 endpos 是自己并上儿子的 endpos,可以用线段树合并来解决。
代码
#include<cstdio>
#include<cstring>
#include<iostream>
#define ll long long
using namespace std;
const int N = 5e5 + 10;
char s[N], t[N];
int n, m, q, L, R, rt[N << 1], ans[N << 1];
struct XD_tree {
int tot, val[N << 6], ls[N << 6], rs[N << 6];
int insert(int x, int l, int r, int pl) {
int now = ++tot; val[now] = val[x]; ls[now] = ls[x]; rs[now] = rs[x];
val[now] = max(val[now], pl);
if (l == r) return now;
int mid = (l + r) >> 1;
if (pl <= mid) ls[now] = insert(ls[now], l, mid, pl);
else rs[now] = insert(rs[now], mid + 1, r, pl);
return now;
}
int query(int now, int l, int r, int L, int R) {
if (!now) return 0;
if (L <= l && r <= R) return val[now];
int mid = (l + r) >> 1, re = 0;
if (L <= mid) re = max(re, query(ls[now], l, mid, L, R));
if (mid < R) re = max(re, query(rs[now], mid + 1, r, L, R));
return re;
}
int merge(int x, int y) {
if (!x || !y) return x + y;
int re = ++tot; val[re] = max(val[x], val[y]);
ls[re] = merge(ls[x], ls[y]); rs[re] = merge(rs[x], rs[y]);
return re;
}
}TT;
struct SAM {
int lst, tot;
struct node {
int son[26], len, fa, pl;
}d[N << 1];
void Init() {
lst = tot = 1;
memset(d[1].son, 0, sizeof(d[1].son));
}
int insert(char c, int pla) {
int p = lst, np = ++tot; lst = np;
memset(d[np].son, 0, sizeof(d[np].son));
d[np].len = d[p].len + 1; d[np].pl = pla;
for (; p && !d[p].son[c]; p = d[p].fa) d[p].son[c] = np;
if (!p) d[np].fa = 1;
else {
int q = d[p].son[c];
if (d[q].len == d[p].len + 1) d[np].fa = q;
else {
int nq = ++tot; d[nq] = d[q];
d[nq].len = d[p].len + 1;
d[np].fa = d[q].fa = nq;
for (; p && d[p].son[c] == q; p = d[p].fa) d[p].son[c] = nq;
}
}
return lst;
}
void build() {
static int tmp[N], xl[N << 1];
for (int i = 1; i <= n; i++) tmp[i] = 0;
for (int i = 1; i <= tot; i++) tmp[d[i].len]++;
for (int i = 1; i <= n; i++) tmp[i] += tmp[i - 1];
for (int i = 1; i <= tot; i++) xl[tmp[d[i].len]--] = i;
for (int i = tot; i >= 1; i--) {//线段树合并来求SAM每个点的endpos
int x = xl[i];
rt[d[x].fa] = TT.merge(rt[d[x].fa], rt[x]);
}
}
void Run(int &now, int &sz, int c) {
while (now) {
if (d[now].son[c]) {
int x = TT.query(rt[d[now].son[c]], 1, n, 1, R) - L + 1;
if (d[d[now].fa].len < x) {//不仅要跳到有这个儿子,还需要这个儿子的位置中有 L~R(前面这个比较的要小于这样减的时候【DP】才会是正的)
now = d[now].son[c]; sz = min(sz + 1, x);
return ;
}
}
now = d[now].fa; sz = d[now].len;
}
now = 1; sz = 0;
}
ll work() {
ll re = 0;
for (int i = 2; i <= tot; i++)
re += max(d[i].len - max(d[d[i].fa].len, ans[d[i].pl]), 0);
return re;
}
}S, T;
int main() {
scanf("%s", s + 1); n = strlen(s + 1);
S.Init();
for (int i = 1; i <= n; i++) {
int pl = S.insert(s[i] - 'a', 0);
rt[pl] = TT.insert(rt[pl], 1, n, i);
}
S.build();
scanf("%d", &q);
while (q--) {
scanf("%s", t + 1); m = strlen(t + 1);
scanf("%d %d", &L, &R);
T.Init();
for (int i = 1; i <= m; i++)
T.insert(t[i] - 'a', i);
int now = 1, sz = 0;
for (int i = 1; i <= m; i++) {
S.Run(now, sz, t[i] - 'a');
ans[i] = sz;
}
printf("%lld\n", T.work());
}
return 0;
}