• Codeforces 1063F


    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-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;
    }
    
  • 相关阅读:
    前端验证银行卡(Luhn校验算法)
    常见的三狼布局
    项目中的分页插件
    JavaScript葵花宝典之闭包
    CSS 高级布局技巧
    window.open打开新窗口被浏览器拦截的处理方法
    Input输入框输入银行卡号自动空格
    关于NSURLSession的上传和下载
    将一个字符串从第40个字节开始替换为@"..."
    IOS开发之高级功能---远程推送
  • 原文地址:https://www.cnblogs.com/ET2006/p/Codeforces-1063F.html
Copyright © 2020-2023  润新知