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
S1、S2 且
∣
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
i、j 时,
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
lower−bound (我用的是线段树上二分)去找到离询问的左端点最近的关键位置。
然后考虑当区间右端点
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 加二分来实现。