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

本文网址:https://www.cnblogs.com/zsc985246/p/17362479.html ,转载请注明出处。

前言

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

补充知识:本原平方串

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

性质:字符串 S 的子串中本原平方串的个数至多为 nlogn

不会 Runs这里只需掌握求解 Runs。可以只读 "0. 初步的定义 & 约定" 和 "4. Runs 相关计算"。

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

题目大意

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

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

  2. 每个子串不循环

  3. 划分出的相邻子串不同。即对于每个 1i<ksisi+1

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

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

1n2×105

思路

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

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

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

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

O(n2) 做法

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

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

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

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

fi=j=1i1(1)F(S[j+1,i])×fj

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

O(nlogn) 做法

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

我们知道,一个 Runs(l,r,p) 中,令满足 i=l+2kp,ir,kZi 为关键点,那么相邻两个关键点之间就是一个本原平方串。

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

具体计算时,需要用 gi 记录编号为 iRuns 的所有本原平方串的贡献之和。

实现细节

注意数组大小,记录 Runs 的数组和 g 数组需要 3.6×106

用自然溢出 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;
}

尾声

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

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

posted @   zsc985246  阅读(299)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示