神仙题,做了我整整 2.5h,写篇题解纪念下逝去的中午
后排膜拜 1 年前就独立切掉此题的 ymx,我在 2021 年的第 5270 个小时 A 掉此题,而 ymx 在 2020 年的第 5270 就已经 A 掉了此题 %%%%%%
首先注意到一件事情,就是如果存在一个长度为 (k) 的 Journey,那么必然存在一个长度为 (k) 的 Journey,满足相邻两个字符串长度的差刚好为 (1)(方便起见,在后文中我们及其为 Condition),具体构造就是,如果存在一个字符串 (s_k) 满足 (|s_{k}|-|s_{k+1}|ge 2),那么我们就找到 (s_{k+1}) 在 (s_k) 中出现的位置,将 (s_{k}) 除了 (s_{k+1}) 之外的其他字符全部去掉,如果 (s_{k+1}) 在 (s_k) 出现的位置左边还有字符就令 (s_k) 往左延伸一格,否则 (s_k) 往右延伸一格,如此进行下去直到不存在这样的 (k) 即可,不难证明这样得到的还是一个长度为 (k) 的 Journey。
仿照 CF700E Cool Slogans 的套路,我们可以设计出一个 (dp):(dp_{i}) 表示 (s[i...n]) 的后缀中,第一个字符串的开头位置为 (i),并且长度最长的满足 Condition 的 Journey 的长度是多少。显然,根据前面的分析,我们只关心满足 (|s_k|-|s_{k+1}|=1) 的 Journey,因此满足条件的 Journey 的第一个字符串必然是 (s[i...i+dp_i-1])。而且我们还可以发现,如果存在一个满足 Condition 的 Journey 第一个字符串是 (s[l...r](r-lge 1)),那么必然存在一个满足 Condition 的 Journey 第一个字符串是 (s[l...r-1]),具体证明大概也是掐头去尾,读者有兴趣自己不妨去证证(?)。因此转移大概就枚举下一个字符串的开头 (j<i),分两种情况:
- 下一个字符加在结尾,我们假设下一个字符串为 (s[j...r]),那么 (s[j+1...r]) 应与上一个字符串,也就是以 (i) 开头的某段长度 (le dp_i) 的子串相同,那么这前面一段显然最长长不过 (min( ext{LCP}(s[i...n],s[j...n]),dp_i)),用这种情况转移得到下一个字符串的长度也不能超过 (min( ext{LCP}(s[i...n],s[j...n]),dp_i)+1),又因为子串不能相交,所以下一个字符串的结束位置必须 (<i),即长度不能超过 (i-j),因此我们有 (dp_jleftarrowmin(min( ext{LCP}(s[i...n],s[j...n]),dp_i)+1,i-j))
- 下一个字符加在开头,类似地,后面一段应与上一个字符串相同,仿照上面的推理过程我们也可以得到 (dp_jleftarrowmin(min( ext{LCP}(s[i...n],s[j+1...n]),dp_i)+1,i-j))
( ext{LCP}) 可以后缀数组预处理,暴力转移是 (mathcal O(n)) 的,因此这个做法复杂度 (mathcal O(n^2))。
附:(mathcal O(n^2)) ( ext{TLE}) 的代码:
const int MAXN=5e5;
const int LOG_N=19;
int n;char s[MAXN+5];pii x[MAXN+5];
int sa[MAXN+5],rk[MAXN+5],ht[MAXN+5],seq[MAXN+5],buc[MAXN+5];
void getsa(){
int vmax=122,gr=0;
for(int i=1;i<=n;i++) buc[s[i]]++;
for(int i=1;i<=vmax;i++) buc[i]+=buc[i-1];
for(int i=n;i;i--) sa[buc[s[i]]--]=i;
for(int i=1;i<=n;i++){
if(s[sa[i]]!=s[sa[i-1]]) ++gr;
rk[sa[i]]=gr;
} vmax=gr;
for(int k=1;k<=n;k<<=1){
for(int i=1;i<=n;i++){
if(i+k<=n) x[i]=mp(rk[i],rk[i+k]);
else x[i]=mp(rk[i],0);
} memset(buc,0,sizeof(buc));int num=0;gr=0;
for(int i=n-k+1;i<=n;i++) seq[++num]=i;
for(int i=1;i<=n;i++) if(sa[i]>k) seq[++num]=sa[i]-k;
for(int i=1;i<=n;i++) buc[x[i].fi]++;
for(int i=1;i<=vmax;i++) buc[i]+=buc[i-1];
for(int i=n;i;i--) sa[buc[x[seq[i]].fi]--]=seq[i];
for(int i=1;i<=n;i++){
if(x[sa[i]]!=x[sa[i-1]]) ++gr;
rk[sa[i]]=gr;
} vmax=gr;if(vmax==n) break;
}
}
void getht(){
int k=0;
for(int i=1;i<=n;i++){
if(rk[i]==1) continue;if(k) --k;int j=sa[rk[i]-1];
while(i+k<=n&&j+k<=n&&s[i+k]==s[j+k]) ++k;
ht[rk[i]]=k;
}
}
int st[MAXN+5][LOG_N+2];
void buildst(){
for(int i=1;i<=n;i++) st[i][0]=ht[i];
for(int i=1;i<=LOG_N;i++) for(int j=1;j+(1<<i)-1<=n;j++)
st[j][i]=min(st[j][i-1],st[j+(1<<i-1)][i-1]);
}
int queryst(int l,int r){
int k=31-__builtin_clz(r-l+1);
return min(st[l][k],st[r-(1<<k)+1][k]);
}
int getlcp(int x,int y){
if(x==y) return n-x+1;
x=rk[x];y=rk[y];if(x>y) swap(x,y);
return queryst(x+1,y);
}
int dp[MAXN+5];
int main(){
scanf("%d%s",&n,s+1);getsa();getht();buildst();int res=0;
for(int i=1;i<=n;i++) dp[i]=1;
for(int i=n;i;i--){
for(int j=1;j<i;j++){
chkmax(dp[j],min(getlcp(i,j)+1,min(i-j,dp[i]+1)));
if(j+1!=i) chkmax(dp[j],min(min(getlcp(i,j+1),dp[i])+1,i-j));
} chkmax(res,dp[i]);
// printf("%d %d
",i,dp[i]);
} printf("%d
",res);
return 0;
}
接下来考虑优化,首先看到这样的限制你可以想到一些奇奇怪怪的方式进行优化,包括但不限于笛卡尔树、单调栈、树套树等,但都没有用(bushi,ymx 去年笛卡尔树切掉了这道题)。因此我们不妨换个角度,从 (dp) 的单调性入手解决这个问题,不难发现一个性质,就是 (dp_{i}le dp_{i+1}+1),具体证明大概就如果存在一个满足 Condition 的 Journey,其第一个字符串是 (s[l...r](r-lge 1)),那么必然也存在一个满足 Condition 的 Journey 第一个字符串是 (s[l...r-1]),这个仿照前面 (s[l+1...r]) 的证明来就行了,因此如果 (dp_ige dp_{i+1}+2),那么必然存在一个满足 Condition 的 Journey,第一个字符串是 (s[i...i+dp_{i+1}+1]),也存在满足 Condition 且第一个字符串是 (s[i+1...i+dp_{i+1}+1]) 的 Journey,而该字符串长度为 (dp_{i+1}+1),与我们 (dp) 的定义不符,因此不会出现这样的情况(当然你如果只看上面 (dp) 转移方程也可以看出这条性质)
注意到这个柿子长得有点像咱们求 (ht) 时用到的性质,因此考虑仿照求 (ht) 的方法——双针+合法性判定求解 (dp) 数组。我们考虑先令 (dp_i=dp_{i+1}+1),然后不断将 (dp_i) 减一直到存在一个满足 Condition 的 Journey,其第一个字符串是 (s[i...i+dp_i-1]),这样问题可以转化为一个判定性问题。那么怎么判定一个 (dp_i) 是否可行呢?显然我们只需要知道是否有一个 (j) 满足 (min(min( ext{LCP}(s[i...n],s[j...n]),dp_j)+1,j-i)ge dp_i) 或 (min(min( ext{LCP}(s[i+1...n],s[j...n]),dp_j)+1,j-i)ge dp_i) 即可,显然两部分是对称的,这里考虑第一部分,由于我们取的是最小值,所以三个括号里的东西都要 (ge dp_i),(j-ige dp_i) 意味着 (jge i+dp_i),( ext{LCP}(s[i...n],s[j...n])+1ge dp_i) 意味着 (j) 在字典序数组上是一段区间,这段区间显然可以二分+ST 表求出,(dp_j+1ge dp_i) 这个条件无法直接处理,不过既然是判定性问题,我们就求一下满足前两个条件的 (j) 中 (dp_j) 的最大值即可。这个最大值乍一看,两个条件,需要求 (max) 的主席树,虽然复杂度依旧是 1log,但空间过大,考虑更简便的实现,不难发现在我们从右往左 DP 的过程中,(i+dp_i-1) 始终是不降的,因此我们肯定只有插入新元素,没有删除操作,因此如果我们在 (i+dp_i-1) 与 (i+dp_i) 之间画一条 dividing line,那么这个 dividing line 肯定是不断左移的,因此我们以字典序数组上的下标为下标开一棵线段树,维护 dividing line 右边的元素,每次 dividing line 往左移一格就将新加入的元素加入线段树,查询就直接在线段树对应区间查询即可。
时间复杂度严格 (mathcal O(nlog n))
const int MAXN=5e5;
const int LOG_N=19;
int n;char str[MAXN+5];pii x[MAXN+5];
int sa[MAXN+5],rk[MAXN+5],ht[MAXN+5],seq[MAXN+5],buc[MAXN+5];
void getsa(){
int vmax=122,gr=0;
for(int i=1;i<=n;i++) buc[str[i]]++;
for(int i=1;i<=vmax;i++) buc[i]+=buc[i-1];
for(int i=n;i;i--) sa[buc[str[i]]--]=i;
for(int i=1;i<=n;i++){
if(str[sa[i]]!=str[sa[i-1]]) ++gr;
rk[sa[i]]=gr;
} vmax=gr;
for(int k=1;k<=n;k<<=1){
for(int i=1;i<=n;i++){
if(i+k<=n) x[i]=mp(rk[i],rk[i+k]);
else x[i]=mp(rk[i],0);
} memset(buc,0,sizeof(buc));int num=0;gr=0;
for(int i=n-k+1;i<=n;i++) seq[++num]=i;
for(int i=1;i<=n;i++) if(sa[i]>k) seq[++num]=sa[i]-k;
for(int i=1;i<=n;i++) buc[x[i].fi]++;
for(int i=1;i<=vmax;i++) buc[i]+=buc[i-1];
for(int i=n;i;i--) sa[buc[x[seq[i]].fi]--]=seq[i];
for(int i=1;i<=n;i++){
if(x[sa[i]]!=x[sa[i-1]]) ++gr;
rk[sa[i]]=gr;
} vmax=gr;if(vmax==n) break;
}
}
void getht(){
int k=0;
for(int i=1;i<=n;i++){
if(rk[i]==1) continue;if(k) --k;int j=sa[rk[i]-1];
while(i+k<=n&&j+k<=n&&str[i+k]==str[j+k]) ++k;
ht[rk[i]]=k;
}
}
int st[MAXN+5][LOG_N+2];
void buildst(){
for(int i=1;i<=n;i++) st[i][0]=ht[i];
for(int i=1;i<=LOG_N;i++) for(int j=1;j+(1<<i)-1<=n;j++)
st[j][i]=min(st[j][i-1],st[j+(1<<i-1)][i-1]);
}
int queryst(int l,int r){
int k=31-__builtin_clz(r-l+1);
return min(st[l][k],st[r-(1<<k)+1][k]);
}
int getlcp(int x,int y){
if(x==y) return n-x+1;
x=rk[x];y=rk[y];if(x>y) swap(x,y);
return queryst(x+1,y);
}
int dp[MAXN+5];
struct node{int l,r,mx;} s[MAXN*4+5];
void build(int k,int l,int r){
s[k].l=l;s[k].r=r;if(l==r) return;int mid=l+r>>1;
build(k<<1,l,mid);build(k<<1|1,mid+1,r);
}
void modify(int k,int x,int v){
if(s[k].l==s[k].r) return s[k].mx=v,void();
int mid=s[k].l+s[k].r>>1;
if(x<=mid) modify(k<<1,x,v);
else modify(k<<1|1,x,v);
s[k].mx=max(s[k<<1].mx,s[k<<1|1].mx);
}
int query(int k,int l,int r){
if(l<=s[k].l&&s[k].r<=r) return s[k].mx;
int mid=s[k].l+s[k].r>>1;
if(r<=mid) return query(k<<1,l,r);
else if(l>mid) return query(k<<1|1,l,r);
else return max(query(k<<1,l,mid),query(k<<1|1,mid+1,r));
}
pii get_itvl(int x,int len){
x=rk[x];int L=1,R=x-1,l=x,r=x;
while(L<=R){
int mid=L+R>>1;
if(queryst(mid+1,x)>=len) l=mid,R=mid-1;
else L=mid+1;
} L=x+1,R=n;
while(L<=R){
int mid=L+R>>1;
if(queryst(x+1,mid)>=len) r=mid,L=mid+1;
else R=mid-1;
} return mp(l,r);
}
bool check(int x,int v){
if(v==1) return 1;
pii p=get_itvl(x,v-1);
// printf("itvl %d %d
",p.fi,p.se);
if(query(1,p.fi,p.se)+1>=v) return 1;
p=get_itvl(x+1,v-1);
// printf("itvl %d %d
",p.fi,p.se);
// printf("%d
",query(1,p.fi,p.se));
if(query(1,p.fi,p.se)+1>=v) return 1;
return 0;
}
int main(){
scanf("%d%s",&n,str+1);getsa();getht();buildst();
// for(int i=1;i<=n;i++) printf("%d%c",sa[i],"
"[i==n]);
int res=0;build(1,1,n);
for(int i=n;i;i--){
// for(int j=1;j<i;j++){
// chkmax(dp[j],min(min(getlcp(i,j),dp[i])+1,i-j));
// if(j+1!=i) chkmax(dp[j],min(min(getlcp(i,j+1),dp[i])+1,i-j));
// }
dp[i]=dp[i+1]+1;
while(!check(i,dp[i])){
modify(1,rk[i+dp[i]-1],dp[i+dp[i]-1]);
dp[i]--;
}
chkmax(res,dp[i]);
// printf("%d %d
",i,dp[i]);
} printf("%d
",res);
return 0;
}