后缀数组专题

6道题题目链接:点击打开链接

(本篇为后缀数组的训练题的个人ac题解,非正规教学题解,仅供参考。)

(后缀数组在《算法竞赛入门经典训练指南》上有专门的题解,本篇代码部分完全采用此书上的代码作为模板)

题意理解:

A、求一个字符串中重复出现至少两次以上的但不重叠出现的最长子串的长度

B、求一个字符串中重复出现至少K次以上的但可重叠出现的最长子串的长度

C、求两个字符串中最长的公共子串的长度

D、求两个字符串中长度大于等于K的公共子串的总个数

E、求N个字符串的至少在半数以上的字符串中出现过的最长公共子串并输出,如果有多条则排序后输出

F、对于一个长度为N的字符串,切两刀成三段,每段翻转,使整体字典序最小,问两刀该切在哪里,并输出处理完后的字符串。

建议做题顺序:

1、F ,只需要用到sa数组

2、B ,先求sa数组,再求height数组,最后遍历一遍height数组(二分)

3、A ,先求sa数组,再求height数组,最后遍历一遍height数组(二分)

4、C ,将两个字符串拼接成一个字符串,然后求sa数组,再求height数组,最后遍历一遍height数组(二分)

5、E ,将N个字符串拼接成一个字符串,先二分求出最长长度,再按此长度找出符合的字符串输出(二分)

6、D (难)


详细题解

题目:F

题目描述:对于一个长度为N的字符串,切两刀成三段,每段翻转,使整体字典序最小,问两刀该切在哪里,并输出处理完后的字符串。

做法分析:

对于一个序列 10 1 2 3 4,切两刀,使整体最小,可以这么切 10 1 | 2 | 3 4 ,这样10 1逆序后为1 10 ; 2逆序为2 ; 3 4逆序为4 3。实际上这两刀我们可以一刀一刀切,我们可以看做第一刀为主要排序准则,第二刀为次要排序准则,第一刀切的好一定比第一刀没切好更优, 所以整体最优可以转化为局部最优,若想让第一段逆序后最小,可以看做将整个字符串逆序后构造后缀数组sa,那么sa【0】指的就是逆序后能最小的一段字符串的起始点,即sa【0】就是第一刀的位置。以上面的序列为例,将整体字符串反转后为4 3 2 1 10,那么sa【0】的位置在1的位置,因为1 10是最小的, 所以第一刀切在1前面。接下来要在4 3 2 中再切第二刀,所以对4 3 2这个序列再做一次后缀数组得到新的sa数组,由sa【0】得到第二刀的位置。

经过上面的分析,这道切两刀分三段的题就可以拓展到切N刀分N+1段的题。另外有点细节需要注意,因为至少要切2刀分3段,所以第一刀并不是一定是sa【0】,比如sa【0】的位置是这个序列的最后一个,那么第二刀就只能切空气了,所以要再加个判断sa【0】不行的话就sa【1】再不行sa【2】,找到第一刀的合理位置使第二刀有的切的位置。 


代码展示:

#include 
#include 
#include 
#include 
using namespace std;
const int maxn=200010;
int o[maxn],s[maxn],sa[maxn],t[maxn],t2[maxn],c[maxn];
struct node {
	int val,pos;
	bool operator < (const node & x) const {
		return val=0; --i) sa[ --c[x[i]] ]=i;
	//倍增基数排序
	for (int k=1; k<=n; k<<=1) {
		int p=0;
		//先根据sa数组直接排序第二关键词
		for (i=n-k; i=k) y[p++]=sa[i]-k;
		//再排序第一关键词
		for (i=0; i=0; --i) sa[--c[x[y[i]]] ]=y[i];
		//根据sa数组重新构造x数组
		swap(x,y);
		p=1;
		x[sa[0]]=0;
		for (i=1; i=n) break;
		m=p;
	}
}
int main() {
	//freopen("datain.txt","r",stdin);
	ios::sync_with_stdio(false);
	cin.tie(0);
	//读入 
	int n;
	cin>>n;
	for (int i=0; i>o[i]; 			//保留一开始的数值在o数组中 
		tmp[i].val=o[i];	//tmp数组用来排序 
		tmp[i].pos=i;
	}
	//排序并分配名次,同成绩的同一名次 
	sort(tmp,tmp+n);
	s[tmp[0].pos]=0;
	for (int i=1; i=0; i--) cout<ans1; i--) cout<ans2; i--)  cout<

题目:B

题目描述:求一个字符串中重复出现至少K次以上的但可重叠出现的最长子串的长度

做法分析:

构造后缀数组sa,构造height数组,二分遍历长度L,每次分割height数组,保证每一段的height值不小于L,如果这一段中的字符串个数大于等于K,则当前L是符合条件的。

代码展示:

#include 
#include 
#include 
#include 
#include 
using namespace std;
const int maxn=20005;
int s[maxn];
int t[maxn],t2[maxn],sa[maxn],c[maxn];
void build_sa(int n,int m){
	int i,*x=t,*y=t2;
	//初始化基数排序 
	for (i=0;i=0;--i) sa[ --c[x[i]] ]=i;
	//倍增基数排序 
	for (int k=1;k<=n;k<<=1){
		int p=0;
		//先根据sa数组直接排序第二关键词
		for (i=n-k;i=k) y[p++]=sa[i]-k;
		//再排序第一关键词
		for (i=0;i=0;--i) sa[--c[x[y[i]]] ]=y[i];
		//根据sa数组重新构造x数组
		swap(x,y);
		p=1;x[sa[0]]=0;
		for (i=1;i=n) break;
		m=p; 
	}
}
int rank[maxn],height[maxn];
void getHeight(int n){
	int k=0;
	for (int i=0;i=k) return true;
     }
     return false;    
}
int main(){
//	freopen("datain.txt","r",stdin);
	int n,k;
	while(~scanf("%d%d",&n,&k)){
		//读入 
		for (int i=0;i



题目:A

题目描述:求一个字符串中重复出现至少两次以上的但不重叠出现的最长子串的长度

做法分析:

构造后缀数组sa,构造height数组,二分遍历长度L,每次分割height数组,保证每一段的height值不小于L,如果这一段中的存在两个字符串不重叠,则当前L是符合条件的。需要注意的是题目中有一个特别要求,若字符串整个加上一个数等于另一个字符串,则也认为两个字符串是相等的,所以进行一次预处理

for (int i=0;i

代码展示:

#include 
#include 
#include 
#include 
using namespace std;
const int maxn=20005;
int s[maxn];
int t[maxn],t2[maxn],sa[maxn],c[maxn];
void build_sa(int n,int m){
	int i,*x=t,*y=t2;
	//初始化基数排序 
	for (i=0;i=0;--i) sa[ --c[x[i]] ]=i;
	//倍增基数排序 
	for (int k=1;k<=n;k<<=1){
		int p=0;
		//先根据sa数组直接排序第二关键词
		for (i=n-k;i=k) y[p++]=sa[i]-k;
		//再排序第一关键词
		for (i=0;i=0;--i) sa[--c[x[y[i]]] ]=y[i];
		//根据sa数组重新构造x数组
		swap(x,y);
		p=1;x[sa[0]]=0;
		for (i=1;i=n) break;
		m=p; 
	}
}
int rank[maxn],height[maxn];
void getHeight(int n){
	int k=0;
	for (int i=0;ix) return true;
     }
     return false;    
}
int main(){
//	freopen("datain.txt","r",stdin);
	int n;
	while(scanf("%d",&n)==1 && n){
		//读入 
		for (int i=0;i


题目:C

题目描述:求两个字符串中最长的公共子串的长度

做法分析:

将两个字符串拼接成一个字符串,然后求sa数组,再求height数组,最后遍历一遍height数组(二分)

代码展示:

#include 
#include 
#include 
using namespace std;
const int maxn=200010;
int s[maxn];
int t[maxn],t2[maxn],sa[maxn],c[maxn];
int len1;
void build_sa(int n,int m){
	int i,*x=t,*y=t2;
	//初始化基数排序 
	for (i=0;i=0;--i) sa[ --c[x[i]] ]=i;
	//倍增基数排序 
	for (int k=1;k<=n;k<<=1){
		int p=0;
		//先根据sa数组直接排序第二关键词
		for (i=n-k;i=k) y[p++]=sa[i]-k;
		//再排序第一关键词
		for (i=0;i=0;--i) sa[--c[x[y[i]]] ]=y[i];
		//根据sa数组重新构造x数组
		swap(x,y);
		p=1;x[sa[0]]=0;
		for (i=1;i=n) break;
		m=p; 
	}
}
int rank[maxn],height[maxn];
void getHeight(int n){
	int k=0;
	for (int i=0;i=len1) return true;
     }
     return false;    
}
int main(){
//	freopen("datain.txt","r",stdin);
	ios::sync_with_stdio(false);
	cin.tie(0);
	//读入 
	string s1,s2;
	cin>>s1>>s2;
	int n=0;
	for (int i=0;s1[i];i++) s[n++]=s1[i]-'a'+2;
	len1=n;
	s[n++]=1;
	for (int i=0;s2[i];i++) s[n++]=s2[i]-'a'+2;
	//构造数组 
	build_sa(n,30);
	getHeight(n);
	//二分 
	int l=0,r=n,mid=1,ans=0;
	while(l<=r){
		mid=(l+r)/2;
		if (judge(n,mid)) {
			ans=mid;
			l=mid+1;
		}
		else r=mid-1;
	}  
	cout<


题目:E

题目描述:求N个字符串的至少在半数以上的字符串中出现过的最长公共子串并输出,如果有多条则排序后输出

做法分析:

将N个字符串拼接成一个字符串,每个串之间用一个从未出现过的字符分隔,然后求sa数组,再求height数组。

1、先二分求出至少在半数以上的字符串中出现过的最长的公共子串长度Lmax

2、再按长度为Lmax的情况下找符合条件的字符串依次输出

难点分析:判断height【i】中sa[i]和sa【i-1】所处的字符串若是同一个,则说明这是一个字符串自身的公共子串,这种情况不算做出现在一条字符串中;只有sa【i-1】和sa【i】分别处于两个字符串中才令个数增加。所以需要一个pos数组用来存储每一个i表示的字符原先是处于哪个字符串的。细节自行领悟,这里只做简要分析。

代码展示:

#include 
#include 
#include 
#include 
using namespace std;
const int maxn=200010;
int s[maxn]; 
int sa[maxn],t[maxn],t2[maxn],c[maxn],pos[maxn];
void build_sa(int n,int m){
	int i,*x=t,*y=t2;
	//初始化基数排序 
	for (i=0;i=0;--i) sa[ --c[x[i]] ]=i;
	//倍增基数排序 
	for (int k=1;k<=n;k<<=1){
		int p=0;
		//先根据sa数组直接排序第二关键词
		for (i=n-k;i=k) y[p++]=sa[i]-k;
		//再排序第一关键词
		for (i=0;i=0;--i) sa[--c[x[y[i]]] ]=y[i];
		//根据sa数组重新构造x数组
		swap(x,y);
		p=1;x[sa[0]]=0;
		for (i=1;i=n) break;
		m=p; 
	}
}
int height[maxn],rank[maxn];
void getHeight(int n){
	int k=0;
	for (int i=0;i (k/2) ) return true;
		}
	}
	return false;
}
bool print(int n,int l,int k){
	int num=0;
	bool vis[k];
	memset(vis,false,sizeof(vis));
	for (int i=0;i(k/2)) {
				for (int j=0;j>k && k){
		//读入预处理,pos数组用来存储一开始每个字母所在的字符串编号 
		int n=0,tmp=0;
		for (int i=0;i>str;
			for (int j=0;str[j];++j) {
				pos[n]=tmp;
				s[n]=str[j]-'a';
				n++;
			}
			pos[n]=tmp;
			s[n]=30+(tmp++);
			n++; 
		} 
		//构造后缀数组 
		build_sa(n,30+tmp+5);
		getHeight(n);
		//二分找到出现在半数以上的字符串中的最长公共子串的长度ans 
		int l=0,r=n,ans=6;
		while(l<=r){
			int mid=(l+r)/2;
			if (judge(n,mid,k)) {
				ans=mid;
				l=mid+1;
			}
			else r=mid-1;
		} 
		//输出 
		if (ans) print(n,ans,k);
		else cout<<"?"<


题目:D

题目描述:求两个字符串中长度大于等于K的公共子串的总个数

做法分析:

将两个个字符串拼接成一个字符串,串之间用一个从未出现过的字符分隔,然后求sa数组,再求height数组。

之后的做法中用到了单调栈的概念,我这里不具体拓展,如果想搞懂这道题先自行百度下单调栈的概念,掌握其代码实现。

顺序遍历一遍height数组,分段,使每段height值大于等于K,用一个单调栈维护当前状态,存储当前在第一个字符串的个数和在第二个字符串的个数。

这道题挺难理解的,我是死记的,所以放在最后了。。,下面直接展示代码了,自行领悟吧,做了我好几天。。

代码展示:

#include 
#include 
#include 
#include 
using namespace std;
const int maxn=200010;
int s[maxn],sa[maxn],rank[maxn],t[maxn],t2[maxn],height[maxn],c[maxn];
int sta[maxn][2];
void build_sa(int n,int m){
	int i,*x=t,*y=t2;
	//初始化基数排序 
	for (i=0;i=0;--i) sa[ --c[x[i]] ]=i;
	//倍增基数排序 
	for (int k=1;k<=n;k<<=1){
		int p=0;
		//先根据sa数组直接排序第二关键词
		for (i=n-k;i=k) y[p++]=sa[i]-k;
		//再排序第一关键词
		for (i=0;i=0;--i) sa[--c[x[y[i]]] ]=y[i];
		//根据sa数组重新构造x数组
		swap(x,y);
		p=1;x[sa[0]]=0;
		for (i=1;i=n) break;
		m=p; 
	}
}
void getHeight(int n){
	int k=0;
	for (int i=0;i>k && k){
		//读入 
		string s1,s2;
		cin>>s1>>s2;
		int len1=s1.length(),n=0;
		for (int i=0;s1[i];i++) s[n++]=s1[i]-'A'+2;
		s[n++]=1;
		for (int i=0;s2[i];i++) s[n++]=s2[i]-'A'+2;
		s[n++]=0;
		//构造数组 
		build_sa(n,70);
		getHeight(n);
		//预处理 
		for (int i=0;ilen1) ans+=tot; 
			}
		}
		//sa[i-1]在第二个字符串中,sa[i]在第一个字符串中 
		top=0;tot=0;
		for (int i=1;ilen1){ cnt++;tot+=height[i];
				}
				while(top && height[i]<=sta[top-1][0]){
					top--;
					tot+=(height[i]-sta[top][0])*sta[top][1];
					cnt+=sta[top][1];
				}
				sta[top][0]=height[i];
				sta[top++][1]=cnt;
				if (sa[i]

找到一个更好的后缀数组模版,作为通用模版

#include
#include
#include
#include
using namespace std;
const int maxn=20000+100;
const int maxm=1000000+100;
struct SuffixArray {
    int s[maxn];
    int sa[maxn],rnk[maxn],height[maxn];
    int t1[maxn],t2[maxn],c[maxm],n;
    void build_sa(int m) {
        int i,*x=t1,*y=t2;
        for(i=0; i=0; i--) sa[--c[x[i]]]=i;
        for(int k=1; k<=n; k<<=1) {
            int p=0;
            for(i=n-k; i=k) y[p++]=sa[i]-k;
            for(i=0; i=0; i--) sa[--c[x[y[i]]]]=y[i];
            swap(x,y);
            p=1;
            x[sa[0]]=0;
            for(i=1; i=n) break;
            m=p;
        }
    }
    void build_height() {
        int i,j,k=0;
        for(i=0; i=k) return true;
            }
        }
        return false;
    }
    void print(int l,int k) {
        int cnt=0;
        for(int i=1; i=k) {
                    for (int j=0;j>str) {
        for(int i=0; str[i]; i++) sa.s[i]=str[i]-'a'+1;
        sa.n=str.length()+1;
        sa.s[sa.n-1]=0;
        sa.build_sa(100+2);
        sa.build_height();
        int l=0,r=sa.n,maxlen,maxnum;
        while(l<=r) {
            int mid=(l+r)/2;
            if(sa.check(2,mid)) {
                l=mid+1;
                maxnum=mid;
            } else r=mid-1;
        }
        l=0;
        r=sa.n;
        while(l<=r) {
            int mid=(l+r)/2;
            if(sa.check(mid,maxnum)) {
                l=mid+1;
                maxlen=mid;
            } else r=mid-1;
        }
        sa.print(maxlen,maxnum);
        printf("%d\n",maxnum);
    }
    return 0;
}



你可能感兴趣的:(后缀数组专题)