「集训队作业2018」串串划分 题解

发布时间 2023-04-28 16:12:57作者: zsc985246

前言

本文中 \(S[i,j]\) 表示取 \(S\)\(i\)\(j\) 位置连接成的子串。

补充知识:本原平方串

定义:一个字符串 \(S\)本原平方串,当且仅当其循环节长度为 \(\frac{|s|}{2}\)

性质:字符串 \(S\) 的子串中本原平方串的个数至多为 \(n \log n\)

不会 \(\text{Runs}\)这里只需掌握求解 \(\text{Runs}\)。可以只读 0. 初步的定义 & 约定4. Runs 相关计算

纠正文章中一点,\(\text{Runs}\) 用字符串 Hash 复杂度 \(O(n \log n)\),只是常数大,建议用自然溢出 Hash 降低常数。

题目大意

给你一个长度为 \(n\) 的字符串 \(S\),你需要将其划分成若干个不相交的非空连续子串 \(s_1 s_2 \dots s_k\),满足以下条件:

  1. 所有串拼接起来正好是原串。

  2. 每个子串不循环

  3. 划分出的相邻子串不同。即对于每个 \(1 \le i < k\)\(s_i \neq s_{i+1}\)

一个字符串 \(S\)循环的,即存在字符串 \(T\) 和整数 \(x\),使得 \(T\) 重复 \(x\) 次恰为 \(S\)。字符串 \(S\) 不循环当且仅当它不是循环的。例如 abab 是循环的,abcab 是不循环的。

你需要计算有多少种不同的划分方式,对 \(998244353\) 取模。

\(1 \le n \le 2 \times 10^5\)

思路

本题显然是不能用公式直接计算的,所以考虑 dp。

限制 \(1\) 很好处理,但是限制 \(2,3\) 有些棘手。

可以发现,对于一个循环串(这里举循环节个数为 \(2\) 的例子)\(S=TT\),我们一定有两种不合法的划分 \(S=TT\)(不满足限制 \(2\)) 和 \(S=T|T\)(不满足限制 \(3\))。其中 | 表示划分。

进一步可以发现,这涵盖了所有不合法情况。而且对于任意循环串,这样的划分一定成对出现。

\(O(n^2)\) 做法

(两种做法较为独立,可以直接看正解)

我们可以设计一个权值,让这两种不合法情况互相抵消。

我们定义字符串 \(S\) 的循环次数 \(C(S)\)\(S\) 由其最小循环节循环几次形成。那么权值为 \(F(S)=(-1)^{C(S)+1}\)。这样正好可以让合法字符串权值为 \(1\),又可以让不合法字符串的权值相互抵消。

这样处理之后,我们终于可以 dp 了。设 \(f_i\) 划分 \(i\) 次的划分方案。用上面设计的权值作容斥系数,可以列出如下方程:

\[f_i=\sum_{j=1}^{i-1} (-1)^{F(S[j+1,i])} \times f_j \]

但是这样最多做到 \(O(n^2)\),因为需要处理 \(F\) 函数。

\(O(n \log n)\) 做法

实际上,进一步思考可以发现,循环节个数为 \(2\) 的循环串(即本原平方串)产生的不合法情况其实已经涵盖了所有不合法情况。

我们知道,一个 \(\text{Runs}(l,r,p)\) 中,令满足 \(i = l + 2kp,i \le r,k \in \mathbb{Z}\)\(i\) 为关键点,那么相邻两个关键点之间就是一个本原平方串。

那么我们只需要找出所有的 \(\text{Runs}(l,r,p)\),进而找到所有本原平方串的结尾与长度。定义 \(f_i\) 为前 \(i\) 个字符的划分方案数。转移时用总方案数减去以 \(i\) 结尾的 \(\text{Runs}\) 对应产生的不合法方案数即可。

具体计算时,需要用 \(g_i\) 记录编号为 \(i\)\(\text{Runs}\) 的所有本原平方串的贡献之和。

实现细节

注意数组大小,记录 Runs 的数组和 \(g\) 数组需要 \(3.6 \times 10^6\)

用自然溢出 Hash 降低常数。

如果常数实在太大,开 int 卡常,但要注意转成 long long 计算取模。

代码实现

#include<bits/stdc++.h>
#define ll long long
#define For(i,a,b) for(ll i=(a);i<=(b);++i)
#define Rep(i,a,b) for(ll i=(a);i>=(b);--i)
const ll N=2e5+10;
const ll mod=998244353;
using namespace std;

ll n;
char a[N];

//字符串Hash
ll b[N],h[N];
void init(char a[],ll n){//Hash预处理
	b[0]=1;
	For(i,1,n){//自然溢出法(卡常)
		b[i]=b[i-1]*131;
		h[i]=(h[i-1]*131+a[i]-'a');
	}
}
ll Hash(ll l,ll r){//求Hash值
	if(l>r)swap(l,r);
	return h[r]-h[l-1]*b[r-l+1];
}

ll lcp(ll x,ll y){
	ll l=1,r=n-max(x,y)+1;
	while(l<=r){
		ll mid=(l+r)>>1;
		if(Hash(x,x+mid-1)==Hash(y,y+mid-1))l=mid+1;
		else r=mid-1;
	}
	return r;
}
ll lcs(ll x,ll y){
	ll l=1,r=min(x,y);
	while(l<=r){
		ll mid=(l+r)>>1;
		if(Hash(x-mid+1,x)==Hash(y-mid+1,y))l=mid+1;
		else r=mid-1;
	}
	return r;
}

ll ly[N];//从i开始的Lyndon串长度
ll tot;//Runs的个数
ll len[N*18];//Runs的长度(n*log n个Runs,不要开小了)
set<ll>vis;//Runs去重
vector<ll>tmp[N];
bool cmp(ll x,ll y){
	ll len=lcp(x,y);//lcp
	return a[x+len]<a[y+len];
}
void get_runs(ll opt){//opt=0是正向字典序,opt=1是反向字典序
	//求Lyndon数组
	Rep(i,n-1,1){
		ly[i]=i+1;
		while(ly[i]&&cmp(ly[i],i)==opt)ly[i]=ly[ly[i]];
	}
	//找Runs
	For(k,1,n-1){//枚举Lyndon循环节
		if(!ly[k])continue;
		ll x=k,y=ly[k];//循环节的左右端点
		ll t1=lcs(x,y),t2=lcp(x,y);//左右能够完全匹配的长度
		ll l=x-t1+1,r=y+t2-1,p=y-x;
		if(t1+t2>p&&vis.insert(r*1919810+l).second){//找到新的Runs
			for(ll i=l-1;i<l+2*p-1&&i+2*p<=r;++i){
				len[++tot]=p;//记录Runs的长度
				for(ll j=i+2*p;j<=r;j+=2*p)tmp[j].push_back(tot);//记录本原平方串的结尾
			}
		}
	}
}

ll f[N],g[N*18];//注意数组大小

void mian(){
	
	scanf("%s",a+1);
	n=strlen(a+1);
	init(a,n);//Hash预处理
	get_runs(0),get_runs(1);//两个字典序都跑一遍
	
	ll s=1;
	f[0]=1;
	For(i,1,n){
		f[i]=s;
		for(auto x:tmp[i]){//从以i结尾的本原平方串转移
			ll p=2*len[x];//本原平方串的长度
			g[x]=(g[x]+f[i-p])%mod;//统计对应Runs的答案
			f[i]=(f[i]-2*g[x]+2*mod)%mod;//减两次是因为产生两个不合法方案
		}
		s=(s+f[i])%mod;//前缀和,统计总方案数
	}
	printf("%lld",f[n]);
	
}

int main(){
	int T=1;
//	scanf("%d",&T);
	while(T--)mian();
	return 0;
}

尾声

如果你发现了问题,你可以直接回复这篇题解

如果你有更好的想法,也可以直接回复!