【UOJ 429】串串划分(Runs)(容斥)+ 有关 Lyndon Tree 及其应用的小小记录

串串划分

题目链接:UOJ 429

题目大意

给你一个字符串,然后问你有多少个划分,满足相邻的两个串不相等,且每个串都不存在整周期。

思路

Lyndon Tree

\(\rm Lyndon\ Tree\) 是什么呢?
它就是你不是可以通过把两个串 \(\rm Ly\) 拼起来得到新的 \(\rm Ly\) 吗,那我们对于一个串找它最小的后缀(不能算它自己),然后得到两个串,然后一直划分下去,就得到了 \(\rm Lyndon\ Tree\)

然后这个东西是建立在这个串是 \(\rm Ly\) 的基础上的,如果不是的话就是对于 \(\rm Ly\) 划分的每个串都这么弄,形成一个森林。

然后你思考一下会发现它其实是 \(\rm SA\)\(\rm rank\) 数组建笛卡尔树得到的树。

然后有一些性质:
如果 \([l,r]\)\(\rm Ly\),那 \(l,r\) 字符对应点在树上的 \(\rm LCA\)\(a\)(设),对于子串 \([l_a,r_a]\),那会有 \(l=l_a\leqslant r_a\leqslant r\)
然后如果这个区间是 \(\rm {Ly}_{s,f}(l)\)(不难感受到这个树也是分 \(f\) 的),那一定是在右儿子(因为是最长的 \(\rm Ly\) 啊)
然后差不多的道理,任意一个 \(\rm run\)\(\rm LyR\) 一定会在右儿子出现(这个就在 \(f\in\{0,1\}\) 的其中一个),然后好像是看那个满足 \(s_{r+1}<_fs_{r+1-p}\)


似乎之前没有说过 \(\rm WPL\)\(\rm PL\),这里提一句把。

对于一个串 \(S\)\(p,q\) 两个周期,如果 \(p+q\leqslant |S|\),那么还有 \(\gcd(p,q)\) 的周期(\(\rm WPL\)\(\rm Weak\ Periodicity\ Lemma\)
(这个你把一个位置用两个周期移一下可以得到减的位置,然后辗转相除)

然后其实可以扩展到 \(p+q-\gcd(p,q)\leqslant |S|\),就是 \(\rm PL\)\(\rm Periodicity Lemma\) 了。
(不会证,好像可以先举例得到是上界,然后比例巴拉搞一通)


有什么用呢,可以搞一个叫做“子串半周期查询”的东西(Two-Period Queries)

这个东西是要你快速查询一个子串是否有不超过长度一半的周期,如果有输出最小的。

我们定义一个 \({\rm exrun}(i,j)\) 为满足 \(i'\leqslant i,j\leqslant j',p\leqslant(j-i+1)/2\) 的一个 \(\rm run(i',j',p)\)
然后我们可以证出它如果存在就是唯一。
(因为如果不是唯一,重叠的那些可以用 \(\rm WPL\) 发现矛盾)

那这个定义就可以解决上面的问题,我们考虑对 \(f\in\{0,1\}\) 都建出 \(\rm{Lyndon\ Tree}\),然后让 \(a={\rm lca}([i,\left\lceil(i+j)/2\right\rceil])\),然后判断右儿子是不是满足条件就可以了。

至于有这个位置可以判就是因为一定有对应的 \(\rm LyR\) 包含 \(\left\lceil(i+j)/2\right\rceil\),那根据上面的性质,就会在某个点的右儿子位置出现。
然后我们会发现 \(a\) 的位置也会 \(>p\),也包含那个位置,所以 \(a\) 会是这个 \(\rm LyR\) 的祖先。
那只要它右儿子不是那个 \(\rm LyR\),那也会是它祖先。

那都是右儿子可以得到位置依次变大。
如果中间那个的 \(\leqslant j\),那它对应的位置有 \(p\) 这个周期,那它就不是 \(\rm Ly\) 了,矛盾。
如果 \(>j\),那个 \(\rm LyR\) 的左边界跟它的右边界形成的区间会小于它形成的区间,那也跟它是 \(\rm Ly\) 矛盾了(定义上)。
所以就反证了。

这道题

这道题还是很友好滴!
(至少不用树状数组)

考虑找不合法的是怎样,看到划分考虑给每一种划分出的字符串一个函数,然后一个划分的权值就是所有的乘起来(这样出现不合法的就直接给 \(0\)
然后发现如果只是要没有整周期就可以弄,但是有说两个之间不能是相同的。

考虑到这个相同的很相似,你如果把两个相同的并起来就形成了前面的的整周期。
所以你考虑容斥掉,就整周期的情况跟两个之间的抵消掉。
然后思考一下不难发现容斥系数就是这个串它最小循环节的出现次数。

那考虑 DP,不难得到 \(n^2\) 的形式(枚举新的子串是从哪里开始),考虑优化。
那首先特别的地方是有整周期的,那别的我们就可以直接前缀和,然后减去有整周期的。

然后考虑整周期,首先把 \(\rm run\) 都找出来,那我们就可以得到一些区间。
发现对于一个 \(\rm run\)\(r\) 它能提供给一个点能从转移的位置是一个等差序列。
那我们只需要对于每个 \(r\) 维护当前出现的每个等差序列它们出现的点的贡献和即可。
然后注意到出现一个点,那别的之前的点的循环节出现次数 \(+1\)(亦或者说全部加一,最后这个从 \(0\)\(1\)),然后你取反一下然后加即可。

代码

#include<cstdio>
#include<vector>
#include<cstring>
#include<algorithm>
#define ll long long
#define mo 998244353
#define ull unsigned long long

using namespace std;

const int N = 2e5 + 100;
char s[N];
int n, ly[N], m;
struct RUNS {
	int x, y, p;
}runs[N << 1];
vector <int> ed[N];
vector <ll> sum[N << 1][2];
//0:减去前缀和
//1:容斥 
ll f[N];

struct HASH {
	const int di = 131;
	ull mi[N], hash[N];
	
	void Init() {
		mi[0] = 1;
		for (int i = 1; i <= n; i++) {
			mi[i] = mi[i - 1] * di;
			hash[i] = hash[i - 1] * di + s[i] - 'a' + 1;
		}
	}
	
	ull get(int x, int y) {
		return hash[y] - hash[x - 1] * mi[y - x + 1];
	}
}H;

int getl(int x, int y) {
	int l = 1, r = min(x, y), re = 0;
	while (l <= r) {
		int mid = (l + r) >> 1;
		if (H.get(x - mid + 1, x) == H.get(y - mid + 1, y)) re = mid, l = mid + 1;
			else r = mid - 1;
	}
	return re;
}

int getr(int x, int y) {
	int l = 1, r = min(n - x + 1, n - y + 1), re = 0;
	while (l <= r) {
		int mid = (l + r) >> 1;
		if (H.get(x, x + mid - 1) == H.get(y, y + mid - 1)) re = mid, l = mid + 1;
			else r = mid - 1;
	}
	return re;
}

int sta[N];

bool cmpS(int x, int y) {
	int pl = getr(x, y);
	return s[x + pl] < s[y + pl];
}

void Lyndon(bool op) {
	sta[0] = 0; sta[++sta[0]] = n + 1; sta[++sta[0]] = n; ly[n] = n;
	for (int i = n - 1; i >= 1; i--) {
		while (sta[0] > 1 && cmpS(i, sta[sta[0]]) == op) sta[0]--;
		sta[++sta[0]] = i; ly[i] = sta[sta[0] - 1] - 1;
	}
}

void check(int x, int y) {
	int l = getl(x, y), r = getr(x, y);
	if (l + r >= y - x + 1) runs[++m] = (RUNS){x - l + 1, y + r - 1, y - x};
}

bool cmp_runs(RUNS x, RUNS y) {
	if (x.x != y.x) return x.x < y.x;
	if (x.y != y.y) return x.y < y.y;
	return x.p < y.p;
}

void Init() {
	H.Init();
	for (int op = 0; op <= 1; op++) {
		Lyndon(op);
		for (int i = 1; i <= n; i++) {
			check(i, ly[i] + 1);
		} 
	}
	sort(runs + 1, runs + m + 1, cmp_runs);
	int tmp = m; m = 0;
	for (int i = 1; i <= tmp; i++)
		if (runs[i].x != runs[i - 1].x || runs[i].y != runs[i - 1].y)
			runs[++m] = runs[i];
} 

int main() {
	scanf("%s", s + 1); n = strlen(s + 1);
	Init();
	
	for (int i = 1; i <= m; i++) {
		for (int r = runs[i].x + 2 * runs[i].p - 1; r <= runs[i].y; r++)
			ed[r].push_back(i);
		int sz = runs[i].p;
		if (runs[i].y - (runs[i].x + 2 * runs[i].p - 1) + 1 <= runs[i].p)
			sz = (runs[i].y - runs[i].x + 1) % runs[i].p;
		sz++;
		sum[i][0].resize(sz); sum[i][1].resize(sz);
	}
	
	f[0] = 1; ll Sum = 1;
	for (int i = 1; i <= n; i++) {
		f[i] = Sum;
		for (int j = 0; j < ed[i].size(); j++) { int v = ed[i][j];
			int l = runs[v].x, r = runs[v].y, p = runs[v].p;
			int id = (i - l + 1) % p;
			(sum[v][0][id] += mo - f[i - 2 * p]) %= mo;
			(sum[v][1][id] *= mo - 1) %= mo; (sum[v][1][id] += mo - f[i - 2 * p]) %= mo;
			(f[i] += sum[v][0][id] + sum[v][1][id]) %= mo;
		}
		(Sum += f[i]) %= mo;
	}
	printf("%lld", f[n]);
	
	return 0;
}
posted @ 2022-07-25 22:02  あおいSakura  阅读(49)  评论(0编辑  收藏  举报