【ybt金牌导航2-3-5】【luogu P4248】子串差异 / 差异

子串差异 / 差异

题目链接: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) lenfaisizei(sizefaisizei)
然而我们真正的时候不能就直接这么上,因为你会发现你 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 sizefaisizei,一开始就是它这个字符串的个数,算完了 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;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值