循环串,常常和\(border\)有关。
一个字符串的\(border\)可以拆分为\(O(logn)\)个等差数列,这个性质常常用于\(DP\)优化。
例题1:回文拆分
这两道题,都可以通过一些转化变为回文区间划分。
首先,建立回文树。
使用\(DP\):设\(f(i)\)表示前\(i\)个字符的回文拆分。设前\(i\)个字符的最长回文后缀为\(u\)。
转移时,枚举\(u\)在回文树上的祖先,暴力进行转移。
这样的复杂度是最坏\(O(n^2)\)的,因为如果字符串有大量循环节(比如全是a)会导致回文树深度很大。
根据回文的对称性,一个回文的回文后缀也是他的一个\(border\)。
因此,根据\(border\)的性质,回文后缀的长度可以划分为\(O(logn)\)个等差数列。依次转移即可。
设这个数列的公差为\(d\)。那么不难发现,在一个节点的父节点用这个等差数列转移时,涉及到的\(f(j)\)刚好比当前所需要的少一个。
那么,对每个树上的节点记录一个\(g\),表示上次用这个节点进行等差数列的转移总和。
这次转移时,把缺少的一项(就是利用这个等差数列中最短的进行转移的那一项)补上,并更新\(g\)值即可。
每次转移\(f\)时都向上跳一个等差数列,复杂度就是对的了。这个可以预处理。
注意不要算重复,同时在在数列长度为1时无需转移。
代码:
int insert(int i,char c)//构建回文树
{
int x=int(c-'a');
while(i-len[la]-1<=0||zf[i-len[la]-1]!=c)
la=fa[la];
if(trs[la][x])
{
la=trs[la][x];
return la;
}
int w=trs[la][x]=++sl,t=fa[la];len[w]=len[la]+2;
while(t&&(trs[t][x]==0||i-len[t]-1<=0||zf[i-len[t]-1]!=c))
t=fa[t];
if(trs[t][x])
{
int f=trs[t][x];
cz[w]=len[w]-len[f];//差值
if(cz[w]==cz[f])
tp[w]=tp[f];
else
tp[w]=f;//预处理跳到的位置
fa[w]=f;
}
else
{
tp[w]=fa[w]=2;
cz[w]=len[w];
}
return la=w;
}
状态转移:
for(int i=1;i<=n;i++)
{
int u=wz[i];
while(u!=2)
{
he[u]=dp[i-len[tp[u]]-cz[u]];//补上最短的那个
if(tp[u]!=fa[u])//用父节点记录的值转移
he[u]=(he[u]+he[fa[u]])%md;
if(i%2==0)//这道题目只用偶串转移
dp[i]=(dp[i]+he[u])%md;
u=tp[u];
}
}
例题2:论战捆竹竿
题意:问一个字符串的所有\(border\)能相加凑成多少不同的数。
由于是凑数问题,因此考虑同余最短路。直接做边数太多显然超时。
仍然将这些数划分为若干等差数列,分别添加。
设等差数列为\(d,d+x,d+2x,……,d+kx\)。
先把模数修改为\(d\)。注意修改时要在每个节点上添加一条权值为原来模数的边。
这样,每个节点都连上\(k\)条边。
根据数论知识,模\(gcd(d,x)\)不同的点互不影响。这样就变为了\(gcd(d,x)\)个环。
每个环从距离最小的位置拆为链,这样就是每个点向一段区间连边。
使用单调队列优化转移即可。时间复杂度\(O(nlogn)\)。
代码:
void chmod(int zz)
{
for(int i=0;i<zz;i++)
jh[i]=inf;
jh[0]=0;
for(int i=0;i<md;i++)
{
ll z=jl[i];
if(z<inf&&z<jh[z%zz])
jh[z%zz]=z;
}
int g=gcd(md,zz);
for(int i=0;i<zz;i++)
jl[i]=jh[i];
for(int s=0;s<g;s++)
{
int i=s,w=s;
do
{
if(jl[i]<jl[w])w=i;
i=(i+md)%zz;
}while(i!=s);
for(i=(w+md)%zz;i!=w;i=(i+md)%zz)
{
ll t=jl[(i-md%zz+zz)%zz]+md;
if(t<jl[i])jl[i]=t;
}
}
md=zz;
}
int dl[500010],he,ta,bh[500010];ll qz[500010];
void insert(int x)
{
while(he<ta&&qz[x]<=qz[dl[ta-1]])
ta-=1;
dl[ta++]=x;
}
void del(int x)
{
if(he<ta&&dl[he]==x)he+=1;
}
void addsl(int d,int x,int k)
{
chmod(d);
if(k==0)return;
int g=gcd(md,x);
for(int s=0;s<g;s++)
{
int i=s,w=s;
do
{
if(jl[i]<jl[w])w=i;
i=(i+x)%md;
}while(i!=s);
for(int i=w,t=0;t<md/g;i=(i+x)%md,t++)bh[i]=t;
he=ta=0;qz[w]=jl[w];
for(i=(w+x)%md;i!=w;i=(i+x)%md)
{
insert((i-x+md)%md);
del((i-(k+1)*x%md+md)%md);
if(he<ta)
{
ll t=qz[dl[he]]+1ll*bh[i]*x+d;
if(t<jl[i])jl[i]=t;
}
qz[i]=jl[i]-1ll*bh[i]*x;
}
}
}
一个字符串的所有整周期循环子串,是可以在\(O(nlogn)\)时间内枚举的。
首先,定义本原平方串为所有形如\(AA\)的串。其中\(A\)不是整周期循环串。
所有本原平方串的个数是\(O(nlogn)\)的。本质不同的本原平方串的个数是\(O(n)\)的。
暴力枚举所有本原平方串:
使用这个做法。
先枚举长度L,然后隔L放置关键点,求出相邻关键点的\(lcp,lcs\)得出长度为\(L\)的平方串的开头位置,将这些位置划分为若干区间。这个可以用后缀数组优化。
对于每个区间\([l,r]\),若\([l,l+L-1]\)这个串循环,那么就忽略这个区间。
否则,枚举\(L\)的倍数\(q\),满足\(l\leq r+2L-2q\),将\([l,l+q-1]\)这个串标记为循环。
枚举\(l\leq i\leq r\),就得到了所有本原平方串。它在这个区间的最大循环次数为\(\frac{r-i}{L}+2\)。
使用哈希表去重即可得到本质不同的。
得到所有本原平方串和他们的重复次数就得到了所有循环串。
例题:
超级毒瘤题。求本质不同的双回文串子串个数。
先只考虑本质不同。对于一个位置他的前后缀的回文前后缀可以拼成一个。使用线段树合并去重。
具体方法见 P5327 [ZJOI2019]语言。
然后,有多种切分方案的双回文串一定是循环串。枚举出来去重即可。
代码7k,不放了。
先根据每个串是否等于下一个来容斥。转移时枚举循环串即可,方法同上。