后缀排序模板

题链

用途

按照LCP的限制枚举子串,求LCP和等

sa

  • \(sa[i]\)表示排名\(i\)的开头
  • \(rk[i],x[i]\)表示\(i\)后缀所在的排名
  • \(y[i]\)表示的第二段排名为\(i\)的开头
  • \(c[i]\)为桶
    倍增
#include<bits/stdc++.h>
using namespace std;

const int N=1e5+5;
int n,m,x[N<<1],y[N<<1],sa[N],c[N],rk[N],hi[N];
char s[N];

int main(){
	scanf("%s",s+1); n=strlen(s+1); m=122;
	for(int i=1;i<=n;i++) c[x[i]=s[i]]++;
	for(int i=2;i<=m;i++) c[i]+=c[i-1];
	for(int i=n;i;i--) sa[c[x[i]]--]=i;
	for(int k=1;k<=n;k<<=1) {
		int num=0;
		for(int i=n-k+1;i<=n;i++) y[++num]=i;
		for(int i=1;i<=n;i++) {
			if(sa[i]>k) y[++num]=sa[i]-k;
		}
		for(int i=1;i<=m;i++) c[i]=0;
		for(int i=1;i<=n;i++) c[x[i]]++;
		for(int i=2;i<=m;i++) c[i]+=c[i-1];
		for(int i=n;i;i--) {
			sa[c[x[y[i]]]--]=y[i],y[i]=0;
		}
		swap(x,y); 
		num=1; x[sa[1]]=1;
		for(int i=2;i<=n;i++) {
			if(y[sa[i]]!=y[sa[i-1]]||y[sa[i]+k]!=y[sa[i-1]+k]) num++;
			x[sa[i]]=num;
		}
		if(num==n) break;
		m=num;
	}
	for(int i=1;i<=n;i++) {
		printf("%d%c",sa[i],i==n?'\n':' ');
	}
}

PS:

  • 别漏了m=num
  • 注意数组开两倍(y可能越界)

LCP,height

  • \(LCP(i,j)\)表示\(sa[i...j]\)的最长公共前缀长度
  • \(height[i]\) 表示\(LCP(i,i-1)\)
  • \(h[i]\)表示\(height[rk[i]]\)

性质1:

\[ LCP(i,k)=min(LCP(i,j),LCP(j,k)),i\leq j\leq k \]

性质2:

\[ LCP(i,k)=min_{i<j\leq k}LCP(j,j-1) \]

性质3:

\[ h[i]>=h[i-1]-1,rk[i]>1 \]

\(feel\)法可证明

	for(int i=1;i<=n;i++) rk[i]=x[i];
	for(int i=1,j,k=0;i<=n;i++) {
		if(rk[i]==1) {
			k=0; continue;
		}
		if(k) k--; j=sa[rk[i]-1];
		while(i+k<=n&&j+k<=n&&s[i+k]==s[j+k]) k++;
		hi[rk[i]]=k;
	}
	for(int i=2;i<=n;i++) {
		printf("%d%c",hi[i],i==n?'\n':' ');
	}

注意:当\(i=1\)是不存在\(h[i]\)

check是否打对:
\(input:\)

\[ ababa \]

\(output:\)

\[Sa: 5 3 1 4 2\\ hi: 0 1 3 0 2 \]

例题1

tyboj 最长公共子串
显然后缀数组的height的应用
将所有字符串和翻转串用独一无二的各不相同的字符隔开,做一遍后缀数组
通过LCP的性质2,可以发现:如果答案为\(Ans\),则排序后的后缀可以根据height[i]与\(Ans\),分成若干块,每块的公共子串长度>=Ans
所以二分答案,然后判断是否有块包含所有字符串或翻转串的子串,时间复杂度\(O(nlgn)\)

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

const int N=1e5+5;
int n,m,cnt,c[N],x[N],y[N],sa[N],rk[N],hi[N],a[N],bl[N],top,st[N];
char s[N];
bool fl[N];

inline bool check(int mid) {
	memset(fl,0,sizeof(fl));
	fl[bl[sa[1]]]=1; int num=1; 
	st[top=1]=bl[sa[1]];
	for(int i=2;i<=n;i++) {
		if(hi[i]>=mid) {
			if(!fl[bl[sa[i]]]) {
				fl[bl[sa[i]]]=1;
				st[++top]=bl[sa[i]];
				num++;
				if(num==cnt) return 1;
			}
		} else {
			for(int j=1;j<=top;j++) fl[st[j]]=0;
			fl[bl[sa[i]]]=1,st[top=1]=bl[sa[i]],num=1;
		}
	}
	return 0;
}

int main(){
	int T; scanf("%d",&T);
	while(T--) {
		scanf("%d",&n); cnt=0; m=122;
		for(int i=1;i<=n;i++) {
			scanf("%s",s+1); int num=strlen(s+1);
			for(int j=1;j<=num;j++) {
				a[++cnt]=s[j];
				bl[cnt]=i;
			}
			a[++cnt]=++m;
			for(int j=num;j;j--) {
				a[++cnt]=s[j];
				bl[cnt]=i;
			}
			a[++cnt]=++m;
		}
		if(n==1){
			printf("%d\n",(int)strlen(s+1));
			continue;
		}
		swap(n,cnt);
		for(int i=1;i<=m;i++) c[i]=0;
		for(int i=1;i<=n;i++) c[x[i]=a[i]]++;
		for(int i=1;i<=m;i++) c[i]+=c[i-1];
		for(int i=n;i;i--) {
			sa[c[x[i]]--]=i;
		} 
		for(int k=1;k<=n;k<<=1) {
			int num=0;
			for(int i=n-k+1;i<=n;i++) y[++num]=i;
			for(int i=1;i<=n;i++) {
				if(sa[i]>k) y[++num]=sa[i]-k;
			}
			for(int i=1;i<=m;i++) c[i]=0;
			for(int i=1;i<=n;i++) c[x[i]]++;
			for(int i=1;i<=m;i++) c[i]+=c[i-1];
			for(int i=n;i;i--) {
				sa[c[x[y[i]]]--]=y[i],y[i]=0;	
			}
			swap(x,y); x[sa[1]]=num=1;
			for(int i=2;i<=n;i++) {
				if(y[sa[i]]!=y[sa[i-1]]||y[sa[i]+k]!=y[sa[i-1]+k]) num++;
				x[sa[i]]=num;
			}
			if(num==n) break;
			m=num;
		}
		for(int i=1;i<=n;i++) rk[i]=x[i];
		for(int i=1,j=0,k=0;i<=n;i++) {
			if(rk[i]==1) {
				k=0; continue;
			}
			if(k) k--;
			j=sa[rk[i]-1];
			while(i+k<=n&&j+k<=n&&a[i+k]==a[j+k]) k++;
			hi[rk[i]]=k;
		}
		int l=0,r=100,ans=0;
		while(l<=r) {
			int mid=l+r>>1;
			if(check(mid)) ans=mid,l=mid+1;
				else r=mid-1;
		}
		printf("%d\n",ans);
	}
	return 0;
}

例题2

ybtoj 公共串计数
老样子拼接字符串求height,sa
对于\(sa[x]\in A,sa[y]\in B\)存在以\(x,y\)为开头的公共串的数目为\(Max(LCP(x,y)-K+1,0)\)
有性质2可以发现,我们可以对\(height[i]>=K\)的进行分段处理,这样就转化成求\(\sum_{x<y}(LCP(x,y)-K+1)\)
按后缀排序后的顺序枚举\(i\),对\(A,B\)分别维护数据结构,对于\(i\),支持所有取min,所有求和
用单调栈维护即可
当然后缀自动机更方便

#include<bits/stdc++.h>
const int INF=1e9;
#define ll long long
using namespace std;

const int N=4e5+5;
int n,m,K,c[N],x[N],y[N],sa[N],hi[N],rk[N],bl[N];
char s[N],s1[N];
ll ans;

void gethi() {
	m=122;
	for(int i=1;i<=m;i++) c[i]=0;
	for(int i=1;i<=n;i++) c[x[i]=s[i]]++;
	for(int i=2;i<=m;i++) c[i]+=c[i-1];
	for(int i=n;i;i--) sa[c[x[i]]--]=i;
	for(int k=1;k<=n;k<<=1) {
		int num=0;
		for(int i=n-k+1;i<=n;i++) y[++num]=i;
		for(int i=1;i<=n;i++) {
			if(sa[i]>k) y[++num]=sa[i]-k;
		}
		for(int i=1;i<=m;i++) c[i]=0;
		for(int i=1;i<=n;i++) c[x[i]]++;
		for(int i=2;i<=m;i++) c[i]+=c[i-1];
		for(int i=n;i;i--) {
			sa[c[x[y[i]]]--]=y[i],y[i]=0;
		}
		swap(x,y);
		x[sa[1]]=num=1;
		for(int i=2;i<=n;i++) {
			if(y[sa[i]]!=y[sa[i-1]]||y[sa[i]+k]!=y[sa[i-1]+k]) num++;
			x[sa[i]]=num;
		}
		if(num==n) break;
		m=num;
	}
	for(int i=1;i<=n;i++) rk[i]=x[i];
	for(int i=1,j,k=0;i<=n;i++) {
		if(rk[i]==1) {k=0; continue; }
		if(k) k--; j=sa[rk[i]-1];
		while(i+k<=n&&j+k<=n&&s[i+k]==s[j+k]) k++;
		hi[rk[i]]=k;
	}
}

struct A{
	int st[N],c[N],top,id; ll s;
	inline void ins(int x,bool k) {
		int sum=0;
		while(top&&st[top]>=x) {
			sum+=c[top];
			if(st[top]!=INF) s-=(ll)c[top]*st[top];
			top--; 
		}
		if(sum) {
			st[++top]=x,c[top]=sum;
			if(x!=INF) s+=(ll)c[top]*st[top];
		}
		if(k) st[++top]=INF,c[top]=1;
	} 
	inline void clear() {
		top=0; s=0;
	}
}st[3];

int main(){
scanf("%d",&K);
while(K) {
	scanf("%s",s1+1); int num=strlen(s1+1); n=0;
	for(int i=1;i<=num;i++) {
		s[++n]=s1[i]; bl[n]=1;
	}
	s[++n]=' ';
	scanf("%s",s1+1); num=strlen(s1+1);
	for(int i=1;i<=num;i++) {
		s[++n]=s1[i]; bl[n]=2;
	}	
	gethi(); st[1].id=1,st[2].id=2;
	ans=0; st[1].clear(),st[2].clear();
	for(int i=2;i<=n;i++) {
		if(hi[i]>=K) {
			st[1].ins(hi[i]-K+1,bl[sa[i]]==1);
			st[2].ins(hi[i]-K+1,bl[sa[i]]==2);
			ans+=st[3-bl[sa[i]]].s;
		} else {
			st[1].clear();
			st[2].clear();
			st[bl[sa[i]]].ins(INF,1);
		}
	}
	printf("%lld\n",ans);
	scanf("%d\n",&K);
}
return 0;
}

例题3

tyboj 后缀求和
明显利用了Height数组,
后缀排序以后,从小到大枚举\(i\),求出所有的和
需要所有取min和所有求和,单调栈
由于需要求出区间height,所以用ST表
Ps: 可能会有相同的后缀位置,忽略即可

#include<bits/stdc++.h>
#define ll long long
const int INF=1e9;
using namespace std;

inline int rd() {
	int ret=0; char ch=getchar();
	while(ch<'0'||ch>'9') ch=getchar();
	while(ch>='0'&&ch<='9') {
		ret=(ret<<1)+(ret<<3)+ch-'0';
		ch=getchar();
	}
	return ret;
}

const int N=1e6+5;
int n,m,c[N],x[N],y[N],sa[N],rk[N],hi[N],lg[N],f[N][25],st[N],top,a[N];
char s[N];

inline void SA() {
	m=122;
	for(int i=1;i<=n;i++) c[x[i]=s[i]]++;
	for(int i=2;i<=m;i++) c[i]+=c[i-1];
	for(int i=n;i;i--) sa[c[x[i]]--]=i;
	for(int k=1;k<=n;k<<=1) {
		int num=0;
		for(int i=n-k+1;i<=n;i++) y[++num]=i;
		for(int i=1;i<=n;i++) {
			if(sa[i]>k) y[++num]=sa[i]-k;
		}
		for(int i=1;i<=m;i++) c[i]=0;
		for(int i=1;i<=n;i++) c[x[i]]++;
		for(int i=2;i<=m;i++) c[i]+=c[i-1];
		for(int i=n;i;i--) {
			sa[c[x[y[i]]]--]=y[i],y[i]=0;
		}
		swap(x,y);
		x[sa[1]]=num=1;
		for(int i=2;i<=n;i++) {
			if(y[sa[i]]!=y[sa[i-1]]||y[sa[i]+k]!=y[sa[i-1]+k]) num++;
			x[sa[i]]=num;
		}
		if(num==n) break;
		m=num;
	} 
	for(int i=1;i<=n;i++) rk[i]=x[i];
	for(int i=1,j,k=0;i<=n;i++) {
		if(rk[i]==1) {
			k=0; continue;
		}
		if(k) k--; j=sa[rk[i]-1];
		while(i+k<=n&&j+k<=n&&s[i+k]==s[j+k]) k++;
		hi[rk[i]]=k;
	}
}

inline void RMQ() {
	lg[0]=-1;
	for(int i=1;i<=n;i++) lg[i]=lg[i>>1]+1,f[i][0]=hi[i];
	for(int j=1;j<=lg[n];j++) {
		for(int i=1;i<=n-(1<<j)+1;i++) {
			f[i][j]=min(f[i][j-1],f[i+(1<<j-1)][j-1]);
		}
	}
}
inline int mn(int i,int j) {
	int k=lg[j-i+1];
	return min(f[i][k],f[j-(1<<k)+1][k]);
} 
int main(){
	n=rd(); int  T=rd(); scanf("%s",s+1);
	SA(); RMQ();
	while(T--) {
		m=rd();
		for(int i=1;i<=m;i++) {
			a[i]=rk[rd()];
		}
		sort(a+1,a+m+1);
		ll ans=0,ss=0;  st[top=1]=INF,c[1]=1;
		for(int i=2;i<=m;i++){
			if(a[i-1]<a[i]) {
			int t=mn(a[i-1]+1,a[i]),sum=0;
			while(top&&st[top]>=t) {
				sum+=c[top];
				if(st[top]!=INF) ss-=(ll)st[top]*c[top];
				top--;
			}
			if(sum) {
				st[++top]=t,c[top]=sum;
				ss+=(ll)t*sum;
			}
			ans+=ss;
			st[++top]=INF,c[top]=1;
			}
		}           
		printf("%lld\n",ans%23333333333333333);
	}
	return 0;
}

例题4

ybtoj 区间颠倒
原题POJ3581
简化题面的时候漏了一个条件,\(a[1]\)为最大值
意味着第一段逆转后字典序必须最小,且要满足分成三段
所以逆转序列后跑一边后缀数组
发现一个长度为\(K\)字符串被分成两端后逆转可以表示成:

也就是取最小的两个逆转的去除第一部分的原字符串拼起来后取长度为\(K\)的一段,一定要跨过中点
做一遍后缀数组即可

#include<bits/stdc++.h>
#define ll long long
const int INF=1e9;
using namespace std;

inline int rd() {
	int ret=0; char ch=getchar();
	while(ch<'0'||ch>'9') ch=getchar();
	while(ch>='0'&&ch<='9') {
		ret=(ret<<1)+(ret<<3)+ch-'0';
		ch=getchar();
	}
	return ret;
}

const int N=4e5+5;
int n,m,c[N],x[N],y[N],sa[N],a[N],s[N];

inline void SA(int n) {
	m=n;
	for(int i=1;i<=m;i++) c[i]=0;
	for(int i=1;i<=n;i++) c[x[i]=s[i]]++;
	for(int i=2;i<=m;i++) c[i]+=c[i-1];
	for(int i=n;i;i--) sa[c[x[i]]--]=i;
	for(int k=1;k<=n;k<<=1) {
		int num=0;
		for(int i=n-k+1;i<=n;i++) y[++num]=i;
		for(int i=1;i<=n;i++) {
			if(sa[i]>k) y[++num]=sa[i]-k;
		}
		for(int i=1;i<=m;i++) c[i]=0;
		for(int i=1;i<=n;i++) c[x[i]]++;
		for(int i=2;i<=m;i++) c[i]+=c[i-1];
		for(int i=n;i;i--) {
			sa[c[x[y[i]]]--]=y[i],y[i]=0;
		}
		swap(x,y);
		x[sa[1]]=num=1;
		for(int i=2;i<=n;i++) {
			if(y[sa[i]]!=y[sa[i-1]]||y[sa[i]+k]!=y[sa[i-1]+k]) num++;
			x[sa[i]]=num;
		}
		if(num==n) break;
		m=num;
	} 
}

struct A{int x,id; }b[N];
bool cmp(A i,A j){return i.x<j.x; }
int fid[N];

int main(){
//	freopen("1.in","r",stdin);
	scanf("%d",&n);
	for(int i=1;i<=n;i++) scanf("%d",&b[i].x),b[i].id=i;
	sort(b+1,b+n+1,cmp);
	a[b[1].id]=m=1; fid[1]=b[1].x;
	for(int i=2;i<=n;i++) {
		if(b[i].x!=b[i-1].x) m++;
		a[b[i].id]=m;
		fid[m]=b[i].x;
	}
	for(int i=1;i<=n;i++) s[n-i+1]=a[i];
	SA(n);  int t;
	for(int i=1;i<=n;i++) {
		if(sa[i]>2) {
			for(int j=1;j<=(n-sa[i]+1)/2;j++) {
				swap(a[j],a[(n-sa[i]+1)-j+1]);
			}
			t=sa[i]-1;
			break;
		}
	}
	for(int i=1;i<=t;i++) {
		s[t+i]=s[i];
	}
	SA(t<<1);
	for(int i=1;i<=t<<1;i++) {
		if(sa[i]<=t&&sa[i]>1) {
			for(int j=sa[i];j<=sa[i]+t-1;j++) {
				a[n-t+j-sa[i]+1]=s[j];
			}
			break;
		} 
	}
	for(int i=1;i<=n;i++) {
		printf("%d\n",fid[a[i]]);
	}
	return 0;
}
posted @ 2021-03-01 12:39  wwwsfff  阅读(105)  评论(0)    收藏  举报