【CF1063F】String Journey(后缀自动机+线段树)

点此看题面

  • 给定一个长度为\(n\)的字符串。
  • 你可以从左到右选出若干无交子串,要求每次选出的字符串必须是前一次的真子串。
  • 问最多能选出多少个字符串。
  • \(n\le5\times10^5\)

翻转字符串

方便起见,我们将原串翻转。

那么现在问题就等价于从左到右选出若干无交子串,要求前一次选出的字符串必须是这次的真子串。

贪心地考虑,让选出的字符串长度从\(1\)开始每次仅增加\(1\)一定不会变劣(显然),因此我们不如强制选出的第\(k\)字符串长度就是\(k\)

动态规划

\(f_i\)表示以\(i\)为最后一个串的结尾,最多选出多少个字符串。

显然,如果\(f_i\)能做到,那么\(f_i-1\)也必然能做到——只要删去长度为\(1\)的串以及剩余每个串开头第一个字符即可。同理,实际上任意小于等于\(f_i\)的答案都能做到。

发现这里有一个基本性质:\(f_i\le f_{i-1}+1\)

证明就是若\(f_i\)能取到更大的值,则\(f_{i-1}\)也一定能取得更大。

于是,实际上我们不需要考虑如何转移,只需要考虑当前的\(f_i\)是否能做到,不能做到就将\(f_i\)\(1\)

验证\(f_i\)能做到,就是要判断是否存在\(j\)满足\(j<i-f_i+1,f_j\ge f_i-1\)且前缀\(j\)长度为\(f_i-1\)的后缀是前缀\(i\)长度为\(f_i\)的后缀的子串。

由于这两个串长度分别是\(f_i-1\)\(f_i\),其实也就等价于前缀\(j\)长度为\(f_i-1\)的后缀等于前缀\(i\)长度为\(f_i-1\)的后缀或者前缀\(i-1\)长度为\(f_{i}-1\)的后缀。

整理一下,就是对于前缀\(i\)长度为\(f_i-1\)的后缀以及前缀\(i-1\)长度为\(f_i-1\)的后缀这两个子串,分别找到最大的满足\(j<i-f_i+1\)且当前询问子串是前缀\(j\)的后缀的\(f_j\),取较大值之后判断是否大于等于\(f_i-1\)

\(f_i\le f_{i-1}+1\)变形得到\(i-f_i\ge i-1-f_{i-1}\),也就是说\(i-f_i\)是不降的。

那么我们可以只考虑每当将\(f_i\)减小\(1\)之后,便将此时的\(i-f_i\)也看作一个可能的\(j\),那么\(j<i-f_i+1\)这一层限制就自然而然没掉了。

剩下的东西可以用后缀自动机来维护了。

后缀自动机

现在的问题就仅仅是对于所有满足当前查询子串是其后缀的前缀\(j\),求出最大的\(f_j\)

如果把这个问题搬到后缀自动机上,我们首先通过倍增找到当前查询子串对应的节点。

则当前子串是前缀\(j\)的一个后缀就充要于它在\(parent\)树上是\(j\)的祖先。

也就是说,我们只要给所有\(j\)对应节点打上一个\(f_j\)的标记,那么一次询问就相当于是求\(parent\)树上对应节点的子树最大值。

只要用线段树维护\(parent\)树的\(dfs\)序列即可。

代码:\(O(nlogn)\)

#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define N 500000
#define LN 20
using namespace std;
int n,Nt,p[N+5],f[N+5];char s[N+5];
class SegmentTree
{
	private:
		#define PT CI l=1,CI r=Nt,CI rt=1
		#define LT l,mid,rt<<1
		#define RT mid+1,r,rt<<1|1
		int Mx[N<<3];
	public:
		I void U(CI x,CI v,PT)//单点修改
		{
			if(Mx[rt]=max(Mx[rt],v),l==r) return;RI mid=l+r>>1;x<=mid?U(x,v,LT):U(x,v,RT);
		}
		I int Q(CI L,CI R,PT)//区间求最大值
		{
			if(L<=l&&r<=R) return Mx[rt];RI mid=l+r>>1;return max(L<=mid?Q(L,R,LT):0,R>mid?Q(L,R,RT):0);
		}
}T;
class SuffixAutomation
{
	private:
		#define add(x,y) (e[++ee].nxt=lnk[x],e[lnk[x]=ee].to=y)
		int lst;struct node {int L,F[LN+5],S[30];}O[N<<1];
		int d,dI[N<<1],dO[N<<1],ee,lnk[N<<1];struct edge {int to,nxt;}e[N<<1];
		I void dfs(CI x)//dfs预处理,记录dfs序
		{
			dI[x]=++d;for(RI i=1;i<=LN;++i) O[x].F[i]=O[O[x].F[i-1]].F[i-1];//顺带预处理倍增数组
			for(RI i=lnk[x];i;i=e[i].nxt) dfs(e[i].to);dO[x]=d;
		}
	public:
		I SuffixAutomation() {Nt=lst=1;}I int Ins(CI x)//插入一个字符,返回对应节点编号
		{
			RI p=lst,o=lst=++Nt;O[o].L=O[p].L+1;
			W(p&&!O[p].S[x]) O[p].S[x]=o,p=O[p].F[0];if(!p) return O[o].F[0]=1,o;
			RI q=O[p].S[x];if(O[p].L+1==O[q].L) return O[o].F[0]=q,o;
			RI k=++Nt;(O[k]=O[q]).L=O[p].L+1,O[o].F[0]=O[q].F[0]=k;
			W(p&&O[p].S[x]==q) O[p].S[x]=k,p=O[p].F[0];return o;
		}
		I void Init() {for(RI i=1;i<=Nt;++i) add(O[i].F[0],i);dfs(1);}//给parent树连边,然后dfs
		I void U(CI x,CI v) {T.U(dI[x],v);}//单点修改
		I int Q(RI x,CI k)//询问前缀x长度为k的后缀
		{
			RI i;for(i=LN;~i;--i) O[O[x].F[i]].L>=k&&(x=O[x].F[i]);return T.Q(dI[x],dO[x]);//倍增找到对应节点,然后子树询问
		}
}S;
int main()
{
	RI i;for(scanf("%d%s",&n,s+1),reverse(s+1,s+n+1),i=1;i<=n;++i) p[i]=S.Ins(s[i]&31);S.Init();//预处理
	#define Check(i) (max(S.Q(p[i],f[i]-1),S.Q(p[i-1],f[i]-1))>=f[i]-1)//检验当前f[i]是否可行
	RI t=1;for(f[1]=1,i=2;i<=n;t=max(t,f[i++]))//枚举每一位DP,t统计答案
		{f[i]=f[i-1]+1;W(f[i]^1&&!Check(i)) --f[i],S.U(p[i-f[i]],f[i-f[i]]);}//不行就将f[i]减1,同时将i-f[i]视作可能的j
	return printf("%d\n",t),0;//输出答案
}
posted @ 2021-01-14 18:34  TheLostWeak  阅读(30)  评论(0编辑  收藏