2012-2013 NEERC, Moscow Subregional Contest D题 & 2021 ICPC Shenyang Regional M 题

M. String Problem

题意:
给一个串,对于每个前缀求字典序最大的后缀。
n < = 1 e 6 n<=1e6 n<=1e6

思路1:
求的是每个前缀的字典序最大的后缀,理所应当地想到,从第 r r r个前缀到第 r + 1 r+1 r+1个前缀,答案会发生什么变化。
假设区间 [ 1 , r ] [1,r] [1,r] 的字典序最大的后缀是 S S S,可以发现在新加入一个字符 s [ r + 1 ] s[r+1] s[r+1] 之后,答案后缀不是 S S S,就是 和 S S S 有着公共前缀后缀的位置,也就是 b o r d e r border border 的位置。( 这个大家可以自行证明,简单画个图应该就可以发现了 )。
同时,还有另一个性质,即 S S S b o r d e r border border 的下一个字符都是非增的。
证明如下:
假设当前前缀的字典序最大后缀是 S S S S S S有两个border S 1 、 S 2 S1、S2 S1S2 ∣ S 1 ∣ < ∣ S 2 ∣ |S1| < |S2| S1<S2,如果 S 1 S1 S1 的下一个字符 小于 S 2 S2 S2 的下一个字符。
在这里插入图片描述

那么就可以发现,当前 后缀 S S S的字典序比 S 1 ′ S1' S1 的后缀要劣,与前面的假设冲突,故得证。
那么知道了这个性质有什么用呢,回到上面的想法,当 r r r指针向后移动了之后,那么 最优后缀 S S S 会变化,当且仅当 S S S b o r d e r border border位置加上这个字符之后,字典序比 S S S 大。也即 将所有 S S S 的公共前缀后缀的前缀的下一个字符 < s [ r + 1 ] <s[r+1] <s[r+1] 的 都不优。有了上面的那个性质,就可以暴力跳到第一个满足 下一个字符 > = s [ r + 1 ] >=s[r+1] >=s[r+1] b o r d e r border border 了。
那么复杂度为什么是对的呢,考虑到每跳一次 b o r d e r border border,可以看成是左指针向右最少移动了 1 1 1,左指针最多只移动 s s s的长度次。
然后对于每个字串都算一次 b o r d e r border border也是不能接受的,可以发现 能够成为答案的都是 当前最优后缀的 b o r d e r border border 位置,而这些位置都是可以重复利用的,不用再去更新,所以只需要再进入 r + 1 r+1 r+1时,对于当前最优串 求一次 s [ r + 1 ] s[r+1] s[r+1] n e x nex nex即可。

代码1:

#include<bits/stdc++.h>
using namespace std;

char s[1000050];
char ans[1000050];//ans存放最优后缀 
int nex[1000050];
int main(){
	scanf("%s",s+1);
	int len=strlen(s+1);
	printf("1 1\n");
	int now=1;nex[1]=0;
	ans[1]=s[1];
	for(int i=2;i<=len;i++){
		while(now&&ans[nex[now]+1]<s[i])now=nex[now];
		ans[++now]=s[i];
		printf("%d %d\n",i-now+1,i);
		nex[now]=nex[now-1];
		while(nex[now]&&ans[nex[now]+1]!=ans[now])nex[now]=nex[nex[now]];//KMP的过程 
		if(now!=1&&ans[nex[now]+1]==ans[now])nex[now]++;
	}
	return 0;
}

思路2:
题目可以转化成 :
给你一个字符串,长度为 n n n,以及 n n n个询问,每次询问 区间 [ 1 , i ] [1,i] [1,i] 中的 字典序最大的后缀。( 废话吗这不是!确实,可以看一下下面那道题,就知道为什么先讲这道题了 )
考虑用后缀数组,就知道每个后缀的排名了,用 r k [ i ] rk[i] rk[i] 代表第 i i i 个后缀的排名。
不难发现 对于 i < j i<j i<j r k [ i ] > r k [ j ] rk[i]>rk[j] rk[i]>rk[j] 那么答案肯定是 i i i ,那么对于一个 区间 的最优后缀 一定是 这个区间中 按照 r k rk rk 升序中的一个后缀。那么对于 i < j i<j i<j r k [ i ] < r k [ j ] rk[i]<rk[j] rk[i]<rk[j],当且仅当 当前区间的右端点 r < j + l c p ( i , j ) r< j+lcp(i,j) r<j+lcp(i,j)。那么开一个长度为 n n n的代表时间轴的 v e c t o r vector vector,用来记录大小关系变化的时间点。
这里有一个细节,用 t m p tmp tmp,代表当前 [ 1 , i ] [1,i] [1,i] 区间的最优后缀,而新进来的后缀 应该和 [ 1 , i ] [1,i] [1,i] r k rk rk 最大值的后缀去计算时间戳。
假设 t t m ttm ttm表示 区间 [ 1 , i ] [1,i] [1,i] r k rk rk最大的后缀,如果去和 t m p tmp tmp比较的话,可能会出现这种情况,即 当前 t m p tmp tmp 已经不优了,这个时候 t t m ttm ttm应该是最优的后缀,因为 i i i t m p tmp tmp计算的时间戳,会提前出现,但是当前 i i i t t m ttm ttm 相比还不够优秀,就导致出错。
同时也可以证明 i i i t t m ttm ttm 计算时间戳,不会出现 i i i t t m ttm ttm 劣 但是先加入的情况。(可以自行分情况证明)

代码2:

#include<bits/stdc++.h>
using namespace std;

const int N=1000050;
int len,q;
char s[N];

vector<int>tim[1000050];
int main(){
	scanf("%s",s+1);
	len=strlen(s+1);
	get_sa(s,len);
	lcp_init(len);
	int tmp=1,ttm=1;
	printf("1 1\n");
	for(int i=2;i<=len;i++){
		if(rk[ttm]<rk[i]){
			tim[i+get_lcp(ttm,i,len)].push_back(i);
			ttm=i;
		}
		for(auto to:tim[i])if(rk[tmp]<rk[to])tmp=to;
		printf("%d %d\n",tmp,i);
	}
	return 0; 
}

t i p s : tips: tips: 这里用后缀数组的 r k rk rk 是便于理解(本人想的时候是用这个想的),但是只用到了两个后缀之间求 l c p lcp lcp,可以用 H a s h Hash Hash 加二分来实现。

D. Darkwing Duck

题意:
给一个串,区间询问字典序最大后缀,可以离线。
n < = 5 e 5 , q < = 5 e 5 n<=5e5,q<=5e5 n<=5e5,q<=5e5

思路:
这道题可以看成是上面问题的加强版,如果是直接来看这道题的话,可以先看上面那道题的思路2;
考虑离线的做法( 蒟蒻只会离线的做法),将所有询问离线,并按照 右端点 r r r 升序排序,那么就可以 从右端点向右移动 最优后缀会怎么变化来考虑。
询问区间字典序最大的后缀,根据上面那道题的结论,每个区间能作为 字典序最优后缀的先决条件是,它在这个区间的 r k rk rk递增序列中。
假设 a n s [ i ] ans[i] ans[i] 代表以 i i i 开头 到当前右端点 r r r 的最优后缀。可以发现 a n s [ i ] ans[i] ans[i] 是 非降的,每个答案覆盖一段区间。
形象的,用 r k rk rk 来理解 a n s [ i ] ans[i] ans[i] ,可以发现 a n s [ i ] ans[i] ans[i] 的最优后缀的 r k rk rk 是 一段段 连续的 r k rk rk递增的后缀,且每个递增 r k rk rk的 最后一个,也就是最大的一个,他们的 r k rk rk 是递减的。可以看成是这样的一张图。(因为 i < j i<j i<j r k [ i ] > r k [ j ] rk[i]>rk[j] rk[i]>rk[j] 那么当询问同时覆盖 i 、 j i、j ij 时, i i i 一定比 j j j 优,也即如果询问的左端点 l < i l<i l<i 那么 a n s [ l ] ans[l] ans[l]肯定不会是 j j j
在这里插入图片描述
如上图,红点代表一个递增序列的终止位置,红点组成的序列是递减的。
假设我们已经处理出来了这个 a n s ans ans数组,可以不用每个都记,只用记关键位置,这样每次询问一个左端点 可以通过 s e t set set l o w e r − b o u n d lower -bound lowerbound (我用的是线段树上二分)去找到离询问的左端点最近的关键位置。
然后考虑当区间右端点 r r r向右移动的时候, a n s ans ans数组会怎么变化。对于 后缀 r r r,它能够影响的区间只有 它和 第一个 r k rk rk 比它的 后缀 之间的区间。因为当询问的区间同时包含 i i i r r r(假设 i < r i<r i<r r k [ i ] > r k [ r ] rk[i]>rk[r] rk[i]>rk[r]) 那么答案肯定不会是 r r r。但是是不是当前 后缀 r r r 要和这个区间的所有 后缀都计算一次时间戳呢? 可以证明也是不需要的,同上面一题的思路2,只需要和这个区间中每个上升序列的最后一个后缀 计算时间戳即可。然后就可以用单调栈来维护 每个上升序列的最后一个后缀。
就简单 A C AC AC了。
详情看代码。

代码:

#include<bits/stdc++.h>
using namespace std;

const int N=500050;
int len,q;
char s[N];

struct node{
	int id,l;
};
vector<node>v[500050];
vector<int>tim[500050];
int ans[500050];
stack<int>st;
int tr[2000060];
void update(int p,int l,int r,int x,int w){
	if(l==r){
		if(w)tr[p]=1;
		else tr[p]=0;
		return ;
	}
	int mid=l+r>>1;
	if(x<=mid)update(2*p,l,mid,x,w);
	else update(2*p+1,mid+1,r,x,w);
	tr[p]=tr[2*p]+tr[2*p+1]; 
}
int query(int p,int l,int r,int x,int y){
	if(x>y)return 0;
	if(x<=l&&r<=y)return tr[p];
	int mid=l+r>>1,ans=0;
	if(x<=mid)ans+=query(2*p,l,mid,x,y);
	if(mid<y)ans+=query(2*p+1,mid+1,r,x,y);
	return ans;
}
int ask(int p,int l,int r,int k){
	if(l==r)return l;
	int mid=l+r>>1;
	if(tr[2*p]>=k)return ask(2*p,l,mid,k);
	return ask(2*p+1,mid+1,r,k-tr[2*p]);
}
int main(){
	scanf("%s",s+1);
	len=strlen(s+1);
	get_sa(s,len);
	scanf("%d",&q);
	lcp_init(len);
	for(int i=1;i<=q;i++){
		int l,r;
		scanf("%d%d",&l,&r);
		v[r].push_back({i,l});
	}
	for(int i=1;i<=len;i++){
		while(!st.empty()&&rk[st.top()]<rk[i]){
			tim[i+get_lcp(st.top(),i,len)].push_back(st.top());
			st.pop();
		}
		st.push(i);
		update(1,1,len,i,1);
		for(auto to:tim[i])update(1,1,len,to,0);
		for(auto to:v[i]){
			int tmp=query(1,1,len,1,to.l-1)+1;
			ans[to.id]=ask(1,1,len,tmp);
		}
	}
	for(int i=1;i<=q;i++)printf("%d\n",ans[i]);
	return 0; 
}

t i p s : tips: tips: 这里用后缀数组的 r k rk rk 是便于理解(本人想的时候是用这个想的),但是只用到了两个后缀之间求 l c p lcp lcp,可以用 H a s h Hash Hash 加二分来实现。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值