Codeforces 题面传送门 & 洛谷题面传送门

神仙题,做了我整整 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;
}

Codeforces 1063F - String Journey(后缀数组+线段树+dp)的更多相关文章

  1. [CF1063F]String Journey[后缀数组+线段树]

    题意 在 \(S\) 中找出 \(t\) 个子串满足 \(t_{i+1}\) 是 \(t_{i}\) 的子串,要让 \(t\) 最大. \(|S| \leq 5\times 10^5\). 分析 定义 ...

  2. BZOJ 1396: 识别子串( 后缀数组 + 线段树 )

    这道题各位大神好像都是用后缀自动机做的?.....蒟蒻就秀秀智商写一写后缀数组解法..... 求出Height数组后, 我们枚举每一位当做子串的开头. 如上图(x, y是height值), Heigh ...

  3. 【XSY1551】往事 广义后缀数组 线段树合并

    题目大意 给你一颗trie树,令\(s_i\)为点\(i\)到根的路径上的字符组成的字符串.求\(max_{u\neq v}(LCP(s_u,s_v)+LCS(s_u,s_v))\) \(LCP=\) ...

  4. Luogu4770 NOI2018你的名字(后缀数组+线段树)

    即求b串有多少个本质不同的非空子串,在a串的给定区间内未出现.即使已经8102年并且马上就9102年了,还是要高举SA伟大旗帜不动摇. 考虑离线,将所有询问串及一开始给的串加分隔符连起来,求出SA.对 ...

  5. BZOJ 2865 字符串识别 | 后缀数组 线段树

    集训讲字符串的时候我唯一想出正解的题-- 链接 BZOJ 2865 题面 给出一个长度为n (n <= 5e5) 的字符串,对于每一位,求包含该位的.最短的.在原串中只出现过一次的子串. 题解 ...

  6. bzoj 1396: 识别子串 && bzoj 2865: 字符串识别【后缀数组+线段树】

    根据height数组的定义,和当前后缀串i最长的相同串的长度就是max(height[i],height[i+1]),这个后缀贡献的最短不同串长度就是len=max(height[i],height[ ...

  7. BZOJ 2865 字符串识别(后缀数组+线段树)

    很容易想到只考虑后缀长度必须为\(max(height[rk[i]],height[rk[i]+1])+1\)(即\([i,i+x-1]\)代表的串只出现过一次)然后我正着做一遍反着做一遍,再取一个\ ...

  8. [CF653F] Paper task - 后缀数组,线段树,vector

    [CF653F] Paper task Description 给定一个括号序列,统计合法的本质不同子串的个数. Solution 很容易想到,只要在传统统计本质不同子串的基础上修改一下即可. 考虑经 ...

  9. Codeforces.700E.Cool Slogans(后缀自动机 线段树合并 DP)

    题目链接 \(Description\) 给定一个字符串\(s[1]\).一个字符串序列\(s[\ ]\)满足\(s[i]\)至少在\(s[i-1]\)中出现过两次(\(i\geq 2\)).求最大的 ...

随机推荐

  1. 二、Ansible基础之模块篇

    目录 1. Ansible Ad-Hoc 命令 1.1 命令格式 1.2 模块类型 1.3 联机帮助 1.3.1 常用帮助参数 1.4 常用模块 1.4.1 command & shell 模 ...

  2. D:\Software\Keil5\ARM\PACK\Keil\STM32F1xx_DFP\2.1.0\Device\Include\stm32f10x.h(483): error: #5: cannot open source input file "core_cm3.h": No such file or directory

    1. 错误提示信息: D:\Software\Keil5\ARM\PACK\Keil\STM32F1xx_DFP\2.1.0\Device\Include\stm32f10x.h(483): erro ...

  3. javascript运算符和表达式

    1.表达式的概念 由运算符连接操作组成的式子,不管式子有多长,最终都是一个值. 2.算术运算符 加+ 减- 乘* 除/ 取模% 负数- 自增++ 自减-- 3.比较运算符 等于==  严格等于=== ...

  4. 通过Envoy实现.NET架构的网关

    什么是Gateway 在微服务体系结构中,如果每个微服务通常都会公开一组精细终结点,这种情况可能会有以下问题 如果没有 API 网关模式,客户端应用将与内部微服务相耦合. 在客户端应用中,单个页面/屏 ...

  5. PCB中,Solder Mask与Paste Mask有啥区别呢?

    Solder Mask Layers: 即阻焊层.顾名思义,他的存在是为了防止PCB在过波峰焊的时候,不应涂锡的地方粘上锡. 可以简单理解为一个洞,该区域(洞)以外的地方,都不允许有焊锡,即只能涂绿油 ...

  6. RMQ、ST表

    ST表 \(\text{ST}\) 表是用于解决可重复贡献问题的数据结构. 可重复贡献问题:区间按位和.区间按位或.区间 \(\gcd\) .区间最大.区间最小等满足结合律且可重复统计的问题. 模板预 ...

  7. 最短路径算法:弗洛伊德(Floyd-Warshall)算法

    一.算法介绍 Floyd-Warshall算法(英语:Floyd-Warshall algorithm),中文亦称弗洛伊德算法,是解决任意两点间的最短路径的一种算法,可以正确处理有向图或负权(但不可存 ...

  8. Ubuntu用apt安装MySQL

    这里以MySQL5.7为例. # 如果之前有安装旧版,先移除sudo apt-get --purge remove mysql-server mysql-client mysql-common # 安 ...

  9. Python Numpy matplotlib Histograms 直方图

    import numpy as np import matplotlib.pyplot as plt mu,sigma = 2,0.5 v = np.random.normal(mu,sigma,10 ...

  10. sort-list leetcode C++

    Sort a linked list in O(n log n) time using constant space complexity. C++ /** * Definition for sing ...