循环串

循环串,常常和\(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,不放了。

先根据每个串是否等于下一个来容斥。转移时枚举循环串即可,方法同上。

posted @ 2021-04-06 22:01  lnzwz  阅读(197)  评论(0编辑  收藏  举报