神仙题,做了我整整 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-l\ge 1)\),那么必然存在一个满足 Condition 的 Journey 第一个字符串是 \(s[l...r-1]\),具体证明大概也是掐头去尾,读者有兴趣自己不妨去证证(?)。因此转移大概就枚举下一个字符串的开头 \(j<i\),分两种情况:
- 下一个字符加在结尾,我们假设下一个字符串为 \(s[j...r]\),那么 \(s[j+1...r]\) 应与上一个字符串,也就是以 \(i\) 开头的某段长度 \(\le dp_i\) 的子串相同,那么这前面一段显然最长长不过 \(\min(\text{LCP}(s[i...n],s[j...n]),dp_i)\),用这种情况转移得到下一个字符串的长度也不能超过 \(\min(\text{LCP}(s[i...n],s[j...n]),dp_i)+1\),又因为子串不能相交,所以下一个字符串的结束位置必须 \(<i\),即长度不能超过 \(i-j\),因此我们有 \(dp_j\leftarrow\min(\min(\text{LCP}(s[i...n],s[j...n]),dp_i)+1,i-j)\)
- 下一个字符加在开头,类似地,后面一段应与上一个字符串相同,仿照上面的推理过程我们也可以得到 \(dp_j\leftarrow\min(\min(\text{LCP}(s[i...n],s[j+1...n]),dp_i)+1,i-j)\)
\(\text{LCP}\) 可以后缀数组预处理,暴力转移是 \(\mathcal O(n)\) 的,因此这个做法复杂度 \(\mathcal O(n^2)\)。
附:\(\mathcal O(n^2)\) \(\text{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\n",i,dp[i]);
} printf("%d\n",res);
return 0;
}
接下来考虑优化,首先看到这样的限制你可以想到一些奇奇怪怪的方式进行优化,包括但不限于笛卡尔树、单调栈、树套树等,但都没有用(bushi,ymx 去年笛卡尔树切掉了这道题)。因此我们不妨换个角度,从 \(dp\) 的单调性入手解决这个问题,不难发现一个性质,就是 \(dp_{i}\le dp_{i+1}+1\),具体证明大概就如果存在一个满足 Condition 的 Journey,其第一个字符串是 \(s[l...r](r-l\ge 1)\),那么必然也存在一个满足 Condition 的 Journey 第一个字符串是 \(s[l...r-1]\),这个仿照前面 \(s[l+1...r]\) 的证明来就行了,因此如果 \(dp_i\ge 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(\text{LCP}(s[i...n],s[j...n]),dp_j)+1,j-i)\ge dp_i\) 或 \(\min(\min(\text{LCP}(s[i+1...n],s[j...n]),dp_j)+1,j-i)\ge dp_i\) 即可,显然两部分是对称的,这里考虑第一部分,由于我们取的是最小值,所以三个括号里的东西都要 \(\ge dp_i\),\(j-i\ge dp_i\) 意味着 \(j\ge i+dp_i\),\(\text{LCP}(s[i...n],s[j...n])+1\ge dp_i\) 意味着 \(j\) 在字典序数组上是一段区间,这段区间显然可以二分+ST 表求出,\(dp_j+1\ge 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(n\log 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\n",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\n",p.fi,p.se);
// printf("%d\n",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]," \n"[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\n",i,dp[i]);
} printf("%d\n",res);
return 0;
}