【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;//输出答案
}