后缀数组学习

不羁岁月 提交于 2019-11-28 11:02:28

后缀数组一个优化是利用$hash$, 直接$O(nlog^2n)$按照定义模拟

#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
#define REP(i,a,n) for(int i=a;i<=n;++i)
#define PER(i,a,n) for(int i=n;i>=a;--i)
using namespace std;
typedef long long ll;
const int P = 1e9+7, INF = 0x3f3f3f3f;
const int N = 1e6+10;
char s[N];
int n, fac[N], f[N];
int Hash(int l, int r) {
	int ret = (f[r]-(ll)f[l-1]*fac[r-l+1])%P;
	if (ret<0) ret+=P;
	return ret;
}
struct _ {
	int l,r,id;
} a[N];
int cmp(_ a, _ b) {
	int l=1,r=min(a.r-a.l+1,b.r-b.l+1),ret=0;
	while (l<=r) {
		int mid=(l+r)/2;
		if (Hash(a.l,a.l+mid-1)==Hash(b.l,b.l+mid-1)) ret=mid,l=mid+1;
		else r=mid-1;
	}
	return s[a.l+ret]<s[b.l+ret];
}
int main() {
	fac[0]=1;
	REP(i,1,N-1) fac[i]=fac[i-1]*991ll%P;
	scanf("%s",s+1);
	n = strlen(s+1);
	REP(i,1,n) f[i] = (f[i-1]*991ll+s[i]-'a'+1)%P;
	REP(i,1,n) a[i].l=i,a[i].r=n,a[i].id=i;
	sort(a+1,a+1+n,cmp);
	REP(i,1,n) printf("%d ",a[i].id);puts("");
}

然后是倍增做法, 每次暴力双关键字排序可以达到复杂度$O(nlog^2n)$.

其中${rk}_i$表示起始位置为$i$的后缀的排名, ${sa}_i$为排名为$i$的后缀的起始位置

#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
#define REP(i,a,n) for(int i=a;i<=n;++i)
#define PER(i,a,n) for(int i=n;i>=a;--i)
#define x first
#define y second
using namespace std;
typedef pair<int,int> pii;
const int N = 1e6+10;
char s[N];
int n, cnt, b[N], sa[N], rk[N];
pii f[N],g[N];

int main() {
	scanf("%s", s+1);
	n = strlen(s+1);
	REP(i,1,n) b[i]=s[i];
	sort(b+1,b+1+n),cnt=unique(b+1,b+1+n)-b-1;
	REP(i,1,n) rk[i]=lower_bound(b+1,b+1+cnt,s[i])-b;
	REP(k,0,30) {
		REP(i,1,n) f[i].x=rk[i],f[i].y=(i+(1<<k))>n?0:rk[i+(1<<k)],g[i]=f[i];
		sort(f+1,f+1+n),cnt=unique(f+1,f+1+n)-f-1;
		REP(i,1,n) rk[i]=lower_bound(f+1,f+1+cnt,g[i])-f;
		if (cnt==n) break;
	}
	REP(i,1,n) sa[rk[i]]=i;
	REP(i,1,n) printf("%d ",sa[i]);puts("");
}

快排过程可以用基数排序来优化, 这样就可以得到$O(nlogn)$求出.

 

通过$sa$可以快速求出求任意两个后缀的$LCP$

记${suf}_i$表示以$i$开头的后缀, $LCP_{i,j}$为${suf}[{sa}_i]$和${suf}[{sa}_j]$的$LCP$的长度

一个结论是${LCP}_{i,j}=\min\limits_{i<k\le j} {LCP}_{k,k-1}$

定义高度数组$h_i={LCP}_{i,i-1}$, 然后用$RMQ$处理一下就可以$O(1)$得出答案

高度数组可以用$hash$按照定义$O(nlogn)$求出, 但是实际上有更好的方法

一个结论是$h[{rk}_i]\ge h[{rk}_{i-1}]-1$, 所以可以$O(n)$求出

$O(nlogn)$求$sa,rk,h$的完整代码如下

int c[N],rk[N],h[N],sa[N];
void build(int *a, int n, int m) {
    int i,*x=rk,*y=h;
    for(i=1;i<=m;i++) c[i]=0;
    for(i=1;i<=n;i++) c[x[i]=a[i]]++;
    for(i=1;i<=m;i++) c[i]+=c[i-1];
    for(i=n;i;i--) sa[c[x[i]]--]=i;
    for(int k=1,p;k<=n;k<<=1) {
        p=0;
        for(i=n-k+1;i<=n;i++) y[++p]=i;
        for(i=1;i<=n;i++) if(sa[i]>k) y[++p]=sa[i]-k;
        for(i=1;i<=m;i++) c[i]=0;
        for(i=1;i<=n;i++) c[x[y[i]]]++;
        for(i=1;i<=m;i++) c[i]+=c[i-1];
        for(i=n;i;i--) sa[c[x[y[i]]]--]=y[i];
        swap(x,y); x[sa[1]]=1; p=1;
        for(i=2;i<=n;i++)
            x[sa[i]]=(y[sa[i-1]]==y[sa[i]] && y[sa[i-1]+k]==y[sa[i]+k])?p:++p;
        if(p==n) break; m=p;
    }
    for(i=1;i<=n;i++) rk[sa[i]]=i;
    for(int i=1,j,k=0;i<=n;i++) {
        if(k) k--;
        j=sa[rk[i]-1];
        while(a[i+k]==a[j+k]) k++;
        h[rk[i]] = k;
    }
}

 

练习1. poj1743

大意: 给定串, 求最长的两个不相交子串, 满足子串相邻位置差值相同. 若答案<5输出0.

差分后就转为求相同的最长不相交子串, 考虑二分答案. 那么在$sa$数组中, 一个$h$值全部$\ge x$的连通块中, 任意两串$lcp$值均$\ge x$, 判断位置差的最大值是否$\ge x$即可.

练习2. poj 3261

大意: 求重复次数至少为$k$的子串最大长度.

二分答案+hash做法很显然. 后缀数组做法跟练习1完全一样, 利用$h$数组性质即可.

 

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!