「集训队作业2018」串串划分 题解
本文网址:https://www.cnblogs.com/zsc985246/p/17362479.html ,转载请注明出处。
前言
本文中 \(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 \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\) 次的划分方案。用上面设计的权值作容斥系数,可以列出如下方程:
但是这样最多做到 \(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;
}
尾声
如果你发现了问题,你可以直接回复这篇题解
如果你有更好的想法,也可以直接回复!