做这道题的时候开始一直在想扫右端点,想了很久没有想出来。但是注意到这道题左右端点是不等价的,扫左端点的话就需要简单很多。
显然,字典序最大的子串必然是以r为结尾的后缀。
那么我们考虑前后往前扫左端点l,显然,以l开头的后缀为最大子串的区间右端点是可以二分的。对于一个点,我们只需要比较之前在这个点的最大子串和以l开头以这个点结尾的子串大小就可以了。
但是这样做是 O(log2n) 的。注意到其实我们可以用一个栈来存以某个点为最大子串的右端点区间,这样的话其实我们只需要对两个点求lcp就行了。而且如果一个区间被删了就永远的被删了,所以时间复杂度就是 O(logn) 的,lcp的话sa或直接hash就行。
注意到这里有 O(logn) 次比较,如果按模phash的话,每次比较出错的概率都是 1P ,所以取p= 108 或 109 就行了。我一开始以为跟hash killer一样(当然其实并不是),需要双hash才行,结果跑了倒数第一。(虽然改了以后还是倒数第一吧。。)
然而将hash改成自然溢出再加个读入优化就变成正数第一了。。
然后膜拜了一下据说是叉姐的题解。
考虑扫右端点的话,维护以一个左端点为最大子串左端点的左端点区间。
但是这时我们就需要求一个奇怪的东西,就是对于i,需要求 j>i,rankj>ranki,min(j+lcp(i,j)) 。但是其实我们只需要求出 j>i,rankj>ranki,min(j) 就可以了,因为如果有 k>j>i,rankj>ranki,rankk>ranki,k+lcp(i,k)<j+lcp(i,j) ,那么就有 lcp(i,k)<lcp(i,j) (考虑sa),即 rankk>rankj>ranki ,就是说我们可以通过j找到i。
所以就变成了每次需要删一棵树。(感觉麻烦很多啊。。为啥比我快那么多。。)
(orz lct1999强化叉姐做法),在上述做法中其实并不会发生每次删一棵树的情况,每次只会删一个点,并不需要树。
因为对于i来说, j>i,rankj>ranki,min(j) 求出来的j与 j>i,rankj>ranki,min(j+lcp(i,j)) 求出来的是相等的。
这里我给出一个不知道对不对的证明。。
考虑反证,设 k>j>i,rankk>ranki,k+lcp(i,k)<j+lcp(i,j).∃/k>k′>j>i,rank′k>ranki,k′+lcp(i,k′)<j+lcp(i,j) ,就是说k是最小的。那么会有 rankk−(j−i)>ranki,lcp(i,k−(j−i))=lcp(i,k) ,显然 k>k−(j−i)>i 。因为j的最小性, k∉(i,j) ;又因为 lcp(i,j)>lcp(i,k)≠lcp(i,k−(j−i)) ,所以 k−(j−i)≠j ;又因为k的最小性, k∉(j,k) 。综上则有 k−(j−i)∉(i,k) ,这便产生了矛盾。所以原命题得证。
代码(比较科学的):
#include<cstdio>
#include<iostream>
using namespace std;
#include<algorithm>
#include<cstring>
#include<cmath>
const int S=1e5+5,Q=1e5+5;
char s[S];
const int base=317,Mod=99997973;
typedef long long LL;
int len=1;
LL pres[S];
LL pwr[S];
LL gethash(int l,int r){
return ((pres[r]-pres[l-1]*pwr[r-l+1])%Mod+Mod)%Mod;
}
int getlcp(int s,int t){//s<t
int l=0,r=len-t+2,mid;//l<=ans<r
while(r-l>1){
mid=l+r>>1;
if(gethash(s,s+mid-1)==gethash(t,t+mid-1))l=mid;
else r=mid;
}
return l;
}
struct QS{
int l,r,i;
bool operator < (const QS & o)const{
return l>o.l;
}
}que[Q];
int ans[Q];
struct SS{
int start,r;//(stack[i+1].r,satck[i].r]
}stack[S];
int main(){
freopen("bzoj_4453.in","r",stdin);
freopen("bzoj_4453.out","w",stdout);
scanf("%s",s+1);
len=strlen(s+1);
for(int i=1;i<=len;++i)pres[i]=(pres[i-1]*base+s[i])%Mod;
pwr[0]=1;
pwr[1]=base;
for(int i=2;i<=len;++i)pwr[i]=pwr[i-1]*base%Mod;
int q;
scanf("%d",&q);
for(int i=0;i<q;++i){
scanf("%d%d",&que[i].l,&que[i].r);
que[i].i=i;
}
sort(que,que+q);
int top=0;
int lcp;
int l,r;
for(int i=len,j=0;i;--i){
//printf("---%d----\n",i);
while(top){
lcp=getlcp(i,stack[top-1].start);
if(i+lcp>stack[top-1].r||s[i+lcp]>s[stack[top-1].start+lcp])--top;
else break;
}
if(top)stack[top]=(SS){i,stack[top-1].start+lcp-1};
else stack[top]=(SS){i,len};
++top;
//for(int j=0;j<top;++j)cout<<stack[j].start<<","<<stack[j].r<<endl;
for(;que[j].l==i;++j){
l=0,r=top;//l<=ans<r
while(r-l>1){
if(stack[l+r>>1].r>=que[j].r)l=l+r>>1;
else r=l+r>>1;
}
ans[que[j].i]=stack[l].start;
}
}
for(int i=0;i<q;++i)printf("%d\n",ans[i]);
}
代码(自然溢出):
#include<cstdio>
#include<iostream>
using namespace std;
#include<algorithm>
#include<cstring>
#include<cmath>
const int S=1e5+5,Q=1e5+5;
char s[S];
const int base=317;
typedef long long LL;
int len;
int pres[S];
int pwr[S];
int gethash(int l,int r){
return pres[r]-pres[l-1]*pwr[r-l+1];
}
int getlcp(int s,int t){//s<t
int l=0,r=len-t+2,mid;//l<=ans<r
while(r-l>1){
mid=l+r>>1;
if(gethash(s,s+mid-1)==gethash(t,t+mid-1))l=mid;
else r=mid;
}
return l;
}
struct QS{
int l,r,i;
bool operator < (const QS & o)const{
return l>o.l;
}
}que[Q];
int ans[Q];
struct SS{
int start,r;//(stack[i+1].r,satck[i].r]
}stack[S];
char * cp=(char *)malloc(2000000);
void in(int &x){
while(*cp<'0'||*cp>'9')++cp;
for(x=0;*cp>='0'&&*cp<='9';)x=x*10+(*cp++^'0');
}
int main(){
freopen("bzoj_4453.in","r",stdin);
freopen("bzoj_4453.out","w",stdout);
fread(cp,1,2000000,stdin);
while(*cp==' '||*cp==0||*cp=='\n'||*cp=='\r')++cp;
while(*cp!=' '&&*cp!=0&&*cp!='\n'&&*cp!='\r')s[++len]=*cp++;
for(int i=1;i<=len;++i)pres[i]=pres[i-1]*base+s[i];
pwr[0]=1;
pwr[1]=base;
for(int i=2;i<=len;++i)pwr[i]=pwr[i-1]*base;
int q;
in(q);
for(int i=0;i<q;++i){
in(que[i].l),in(que[i].r);
que[i].i=i;
}
sort(que,que+q);
int top=0;
int lcp;
int l,r;
for(int i=len,j=0;i;--i){
//printf("---%d----\n",i);
while(top){
lcp=getlcp(i,stack[top-1].start);
if(i+lcp>stack[top-1].r||s[i+lcp]>s[stack[top-1].start+lcp])--top;
else break;
}
if(top)stack[top]=(SS){i,stack[top-1].start+lcp-1};
else stack[top]=(SS){i,len};
++top;
//for(int j=0;j<top;++j)cout<<stack[j].start<<","<<stack[j].r<<endl;
for(;que[j].l==i;++j){
l=0,r=top;//l<=ans<r
while(r-l>1){
if(stack[l+r>>1].r>=que[j].r)l=l+r>>1;
else r=l+r>>1;
}
ans[que[j].i]=stack[l].start;
}
}
for(int i=0;i<q;++i)printf("%d\n",ans[i]);
}
总结:
①左右端点不等价的时候,扫左/右端点都要考虑一下。
②注意lcp的大小和rank的大小之间的关系,这其中会蕴含单调性。
③hash每一次比较的出错概率是 1P ,但如果排序的话,实际上就相当于是进行了 n2 次比较,虽然实际可能只有 nlogn 。