「集训队作业2018」串串划分 题解
本文网址:https://www.cnblogs.com/zsc985246/p/17362479.html ,转载请注明出处。
前言
本文中 表示取 中 到 位置连接成的子串。
补充知识:本原平方串
定义:一个字符串 是本原平方串,当且仅当其循环节长度为 。
性质:字符串 的子串中本原平方串的个数至多为 。
不会 走这里,只需掌握求解 。可以只读 "0. 初步的定义 & 约定" 和 "4. Runs 相关计算"。
纠正文章中一点, 用字符串 Hash 复杂度 ,只是常数大,建议用自然溢出 Hash 降低常数。
题目大意
给你一个长度为 的字符串 ,你需要将其划分成若干个不相交的非空连续子串 ,满足以下条件:
-
所有串拼接起来正好是原串。
-
每个子串不循环。
-
划分出的相邻子串不同。即对于每个 ,。
一个字符串 是循环的,即存在字符串 和整数 ,使得 重复 次恰为 。字符串 不循环当且仅当它不是循环的。例如 "abab" 是循环的,"abcab" 是不循环的。
你需要计算有多少种不同的划分方式,对 取模。
。
思路
本题显然是不能用公式直接计算的,所以考虑 dp。
限制 很好处理,但是限制 有些棘手。
可以发现,对于一个循环串(这里举循环节个数为 的例子),我们一定有两种不合法的划分 (不满足限制 ) 和 (不满足限制 )。其中 |
表示划分。
进一步可以发现,这涵盖了所有不合法情况。而且对于任意循环串,这样的划分一定成对出现。
做法
(两种做法较为独立,可以直接看正解)
我们可以设计一个权值,让这两种不合法情况互相抵消。
我们定义字符串 的循环次数 为 由其最小循环节循环几次形成。那么权值为 。这样正好可以让合法字符串权值为 ,又可以让不合法字符串的权值相互抵消。
这样处理之后,我们终于可以 dp 了。设 划分 次的划分方案。用上面设计的权值作容斥系数,可以列出如下方程:
但是这样最多做到 ,因为需要处理 函数。
做法
实际上,进一步思考可以发现,循环节个数为 的循环串(即本原平方串)产生的不合法情况其实已经涵盖了所有不合法情况。
我们知道,一个 中,令满足 的 为关键点,那么相邻两个关键点之间就是一个本原平方串。
那么我们只需要找出所有的 ,进而找到所有本原平方串的结尾与长度。定义 为前 个字符的划分方案数。转移时用总方案数减去以 结尾的 对应产生的不合法方案数即可。
具体计算时,需要用 记录编号为 的 的所有本原平方串的贡献之和。
实现细节
注意数组大小,记录 Runs 的数组和 数组需要 。
用自然溢出 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;
}
尾声
如果你发现了问题,你可以直接回复这篇题解
如果你有更好的想法,也可以直接回复!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现