子串差异 / 差异
题目链接:ybt金牌导航2-3-5 / luogu P4248
题目大意
给你一个字符串,要你求一个式子:
(Ti 是字符串从第 i 个字符开始的后缀,len(a) 是字符串 a 的长度,lcp(a,b) 是字符串 a,b 的最长公共前缀)
思路
容易看到,前两项可以直接快速求出来,问题就是第三项要怎么搞。
那问题就转换成了求每两个后缀的最长公共前缀的和。
求最长公共前缀可以用 SA 快速搞,但是它要求两两之间的,就似乎不太好搞。
(不过可以枚举 height 数组中作为区间最小的,然后往外扩展,扩展的范围用单调队列来求也行)
但我们这里用的是 SAM。
那你考虑 SAM 要怎么求,同一个后缀要通过跳
f
a
i
fa_i
fai,而前缀就是后缀自动机上的,而且后缀自动机上就是相当于把每个后缀放进去。
那我们不妨把字符串翻转一下,然后就变成了求每两个前缀的最长公共后缀。
那求两个前缀的公共后缀其实就是把它们所在的位置开始跳
f
a
i
fa_i
fai 边,跳到一起。
那如果是最长公共后缀,其实就是跳到
f
a
i
fa_i
fai 构成的树中这两个点 LCA 的位置。
那你可以枚举 LCA,然后求有多少个点对的 LCA 是它,那这个长度就是
l
e
n
L
C
A
len_{LCA}
lenLCA。
那这个求就变成了
O
(
1
)
O(1)
O(1) 的,我们要做的其实就是求出
s
i
z
e
i
size_i
sizei:以
i
i
i 为根的字符串总数。
这个之前讲过,可以通过 DP 得到。
那其实我们枚举
i
i
i,然后 LCA 是
f
a
i
fa_i
fai,它必有
i
i
i 这个子树里面的点的贡献就是
l
e
n
f
a
i
∗
s
i
z
e
i
∗
(
s
i
z
e
f
a
i
−
s
i
z
e
i
)
len_{fa_i}*size_i*(size_{fa_i}-size_i)
lenfai∗sizei∗(sizefai−sizei)
然而我们真正的时候不能就直接这么上,因为你会发现你
a
,
b
a,b
a,b 和
b
,
a
b,a
b,a 算了两次,而题目要求只算一次。
而且你是不能除以二的,因为
L
C
A
(
a
,
b
)
=
a
LCA(a,b)=a
LCA(a,b)=a 的这种情况的
a
,
b
a,b
a,b 是只算了一次的。
那你可以用这样一种方法,你用
w
f
a
i
w_{fa_i}
wfai 代替
s
i
z
e
f
a
i
−
s
i
z
e
i
size_{fa_i}-size_i
sizefai−sizei,一开始就是它这个字符串的个数,算完了
i
i
i 的贡献,再把
s
i
z
e
i
size_i
sizei 加进
w
f
a
i
w_{fa_i}
wfai。
这样的话,每个对就只会算一次。
然后就好了。
代码
#include<cstdio>
#include<cstring>
#define ll long long
using namespace std;
struct node {
int fa, len;
ll size, w;
int son[26];
node() {
fa = len = 0;
size = 0ll;
memset(son, 0, sizeof(son));
}
}d[1000001];
char s[500001];
int n, tot, lst;
ll ans;
void SAM_build(int now) {
int p = lst;
int np = ++tot;
d[np].len = d[p].len + 1;
d[np].size = 1;
d[np].w = 1;
lst = np;
for (; p && !d[p].son[now]; p = d[p].fa)
d[p].son[now] = np;
if (!p) d[np].fa = 1;
else {
int q = d[p].son[now];
if (d[q].len == d[p].len + 1) d[np].fa = q;
else {
int nq = ++tot;
d[nq] = d[q];
d[nq].size = 0;
d[nq].w = 0;
d[nq].len = d[p].len + 1;
d[q].fa = nq;
d[np].fa = nq;
for (; p && d[p].son[now] == q; p = d[p].fa)
d[p].son[now] = nq;
}
}
}
int tmp[500001], tp[1000001];
void get_tp() {
for (int i = 0; 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++)
tp[tmp[d[i].len]--] = i;
}
void DP() {//DP 求从 i 出发的子串个数
for (int i = tot; i >= 1; i--) {
int now = tp[i];
d[d[now].fa].size += d[now].size;
}
}
int main() {
scanf("%s", s + 1);
n = strlen(s + 1);
tot = lst = 1;
for (int i = n; i >= 1; i--)
SAM_build(s[i] - 'a');
get_tp();
DP();
ans = 1ll * (n + 1) * n / 2 * (n - 1);//快速求前两项
for (int i = tot; i >= 1; i--) {
int now = tp[i];//枚举 LCA 的点
ans -= d[d[now].fa].len * d[now].size * d[d[now].fa].w * 2;
d[d[now].fa].w += d[now].size;//它相当于公式里的 d[d[now].fa].size-d[now].size,但不能用它
//因为这样子就会计算重复,而且是不能通过除二来解决的
}
printf("%lld", ans);
return 0;
}