后缀数组题集

sa[i]:排名为i的后缀起始位置是多少。
rank[i]:从 i 位置起始的后缀在所有后缀中字典序排序后序号,与sa互逆。
lcp[i]:S[sa[i] ,…] 与 S[sa[i+1] , …]的最长公共前缀。

Musical Theme

测试地址
题意简述
给n个数组成的串,求是否有多个“相似”且不重叠的子串的长度大于等于5,两个子串相似当且仅当长度相等且每一位的数字差都相等。

解题思路
本题题意有些难理解。就是利用后缀数组+高度数组解决最长不重复子串问题,解法很套路,就是二分+判断。

代码示例

//POJ1743
#include<cmath>
#include<cstdio>
#include<algorithm>
using namespace std;
const int N = 2e4+10;
int n,a[N],b[N];
int getInt() {
	int ans = 0;
	bool neg = false;
	char c = getchar();
	while (c!='-' && (c<'0' || c>'9')) c = getchar();
	if (c == '-') neg = true, c = getchar();
	while (c>='0' && c<='9')
		ans = ans*10 + c-'0', c = getchar();
	return neg ? -ans : ans; 
}
int sa[N],rk[N],lcp[N],tmp[N];
int len,k;
bool compare_sa(int i,int j){
	if(rk[i] != rk[j]) return rk[i] < rk[j];
	int ri = i+k <= len?rk[i+k]:-1;
	int rj = j+k <= len?rk[j+k]:-1;
	return ri < rj;
}
void construct_sa(int S[],int sa[]){
	len = n;
	for(int i = 0;i <= len;i++){
		sa[i] = i;
		rk[i] = i < len?S[i]:-1;
	}
	for(k = 1;k <= n;k *= 2){
		sort(sa,sa+1+len,compare_sa);
		tmp[sa[0]] = 0;
		for(int i = 1;i <= len;i++)
			tmp[sa[i]] = tmp[sa[i-1]]+compare_sa(sa[i-1],sa[i]);
		for(int i = 0;i <= len;i++) rk[i] = tmp[i];
	}
}
void construct_lcp(int S[],int sa[],int lcp[]){
	for(int i = 0;i <= len;i++) rk[sa[i]] = i;
	int h = 0; lcp[0] = 0;
	for(int i = 0;i < len;i++){
		int j = sa[rk[i]-1];
		if(h > 0) h--;
		for(;j + h < len && i + h < len;h++)
			if(S[j+h] != S[i+h]) break;
		lcp[rk[i]-1] = h;
	}
}
bool check(int x){
	int pre = 0;
	if(lcp[1] >= x) pre = 1;
	for(int i = 2;i < len;i++){
		if(!pre && lcp[i] >= x) pre = i;
		if(lcp[i-1] >= x)//注意一下到底是i还是i-1
			if(abs(sa[pre] - sa[i]) > x) return true; 
	}
	return false;
}
void solve(){
	construct_sa(a,sa);
	construct_lcp(a,sa,lcp);
	int l = 0,r = n;
	while(l <= r){
		int mid = l+r>>1;
		if(check(mid)) l = mid+1;
		else r = mid-1;
	}
	if(++r < 5) r = 0;
	printf("%d\n",r);
}
int main(){
	while(scanf("%d",&n) && n){
		//注意,该模板必须从下标0开始存!
		for(int i = 0;i < n;i++) b[i] = getInt();
		for(int i = 1;i < n;i++) a[i] = b[i]-b[i-1]+100;
		a[0] = b[0]+100; solve();
	}
	return 0;
}

Milk Patterns

测试地址
题意简述
求可重叠的,出现k次的最长重复子串。
解题思路
这个就是可重叠的最长重复子串了,要求求出最长的,重复出现k次以上的子串,通过题意以及样例观察可知,是允许子串重复的。解决方法和上题类似,也是后缀数组+二分。

代码示例

//POJ3261
#include<cstdio>
#include<algorithm>
using namespace std;
const int N = 2e4+10;
int n,k,a[N],kk;
int rk[N],sa[N],lcp[N],tmp[N];
bool compare_sa(int i,int j){
	if(rk[i] != rk[j]) return rk[i] < rk[j];
	int ri = i+k <= n?rk[i+k]:-1;
	int rj = j+k <= n?rk[j+k]:-1;
	return ri < rj;
}
void construct_sa(int S[],int sa[]){
	for(int i = 0;i <= n;i++){
		sa[i] = i;
		rk[i] = i < n?S[i]:-1;
	}
	//别把全局变量的k覆盖了!!!
	for(k = 1;k <= n;k *= 2){
		sort(sa,sa+1+n,compare_sa);
		tmp[sa[0]] = 0;
		for(int i = 1;i <= n;i++) 
			tmp[sa[i]] = tmp[sa[i-1]] + compare_sa(sa[i-1],sa[i]);
		for(int i = 0;i <= n;i++) rk[i] = tmp[i];
	}
}
void construct_lcp(int S[],int sa[],int lcp[]){
	for(int i = 0;i <= n;i++) rk[sa[i]] = i;
	int h = 0; lcp[0] = 0;
	for(int i = 0;i < n;i++){
		int j = sa[rk[i]-1];
		if(h > 0) h--;
		for(;j+h < n && i+h < n;h++)
			if(S[j+h] != S[i+h]) break;
		lcp[rk[i]-1] = h;
	}
}
bool check(int x){
	int cnt = 1;
	for(int i = 2;i <= n;i++){
		if(lcp[i-1] >= x) cnt++;
		else{
			if(cnt >= kk) return true;
			cnt = 1;
		} 
	}
	return cnt >= kk;
}
void solve(){
	construct_sa(a,sa);
	construct_lcp(a,sa,lcp);
	int l = 0, r = n;
	//for(int i = 1;i <= n;i++) printf("%d ",lcp[i]);
	while(l <= r){
		int mid = l+r>>1;
		if(check(mid)) l = mid+1;
		else r = mid-1;
	}
	printf("%d\n",r);
}
int main(){
	while(scanf("%d%d",&n,&kk) != EOF){
		for(int i = 0;i < n;i++) scanf("%d",a+i);
		solve();
	}
	return 0;
}

不同子串的个数

测试地址
题意简述
给定一个字符串,求其所有子串中,不同的子串数量。

解题思路
利用后缀数组+高度数组求解。已知高度数组lcp,那么首先可以知道第一个sa[1]有n - sa[1]个不同子串;而2~n,每个sa[i] 有 n - sa[i] - lcp[i-1] 个不同子串。累加即可,注意结果可能爆 int。

代码示例

//P2408不同子串的个数
#include<cstdio>
#include<algorithm>
using namespace std;
typedef long long ll;
const int N = 1e5+10;
char str[N];
int n;
int sa[N],lcp[N],tmp[N],k,rk[N];
bool compare_sa(int i,int j){
	if(rk[i] != rk[j]) return rk[i] < rk[j];
	int ri = i+k <= n?rk[i+k]:-1;
	int rj = j+k <= n?rk[j+k]:-1;
	return ri < rj;
}
void construct_sa(const char *S,int sa[]){
	for(int i = 0;i <= n;i++){
		sa[i] = i;
		rk[i] = i < n?S[i]:-1;
	}
	for(k = 1;k <= n;k *= 2){
		sort(sa,sa+1+n,compare_sa);
		tmp[sa[0]] = 0;
		for(int i = 1;i <= n;i++) 
			tmp[sa[i]] = tmp[sa[i-1]] + compare_sa(sa[i-1],sa[i]);
		for(int i = 0;i <= n;i++) rk[i] = tmp[i];
	}
}
void construct_lcp(const char* S,int sa[],int lcp[]){
	for(int i = 0;i <= n;i++) rk[sa[i]] = i;
	int h = 0; lcp[0] = 0;
	for(int i = 0;i < n;i++){
		int j = sa[rk[i]-1];
		if(h > 0) h--;
		for(;j+h < n && i+h < n;h++)
			if(S[j+h] != S[i+h]) break;
		lcp[rk[i]-1] = h;
	}
}
ll sum = 0;
void calc(){
	sum = n - sa[1];
	for(int i = 2;i <= n;i++)
		sum += n - sa[i] - lcp[i-1];
}
void solve(){
	construct_sa(str,sa);
	construct_lcp(str,sa,lcp);
	calc();
	printf("%lld\n",sum);
}
int main(){
	#ifdef LOCAL
		freopen("123.txt","r",stdin);
		freopen("222.txt","w",stdout);
	#endif
	while(~scanf("%d",&n)){
		scanf("%s",str);
		solve();
	}
	return 0;
}

Substring

测试地址
题意简述
给定一个字符串,求所有不同的,且包含字符x的子串数量。

解题思路
和上一题有些类似,但是我们需要确保每一个被统计的子串包含字符x。那么我们可以用一个数组pos[p]来记录 p 后面第一个字符 x 的位置,那么我们从头开始统计时,sa[1]有n - pos[ sa[1] ] 个合法子串,其它位置有 n - max(pos[ sa[i] ] , sa[i] + lcp[i])个合法子串,将它们累加起来即可。
这题选G++编译器就超时,选C++就可以通过,猜测可能和STL有关。

//Hdu5769,G++超时,C++AC
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
const int N = 1e5+10;
int t,n;
char str[N],lv[3];
int rk[N],sa[N],lcp[N],tmp[N],k;
inline bool compare_sa(int i,int j){
	if(rk[i] != rk[j]) return rk[i] < rk[j];
	int ri = i+k <= n?rk[i+k]:-1;
	int rj = j+k <= n?rk[j+k]:-1;
	return ri < rj;
}
void construct_sa(const char* S,int sa[]){
	for(int i = 0;i <= n;i++){
		sa[i] = i;
		rk[i] = i < n?S[i]:-1;
	}
	for(k = 1;k <= n;k *= 2){
		sort(sa,sa+1+n,compare_sa);
		tmp[sa[0]] = 0;
		for(int i = 1;i <= n;i++)
			tmp[sa[i]] = tmp[sa[i-1]] + compare_sa(sa[i-1],sa[i]);
		for(int i = 0;i <= n;i++) rk[i] = tmp[i];
	}
}
void construct_lcp(const char* S,int sa[],int lcp[]){
	for(int i = 0;i <= n;i++) rk[sa[i]] = i;
	int h = 0; lcp[0] = 0;
	for(int i = 0;i < n;i++){
		int j = sa[rk[i]-1];
		if(h > 0) h--;
		for(;j+h < n && i+h < n;h++)
			if(S[j+h] != S[i+h]) break;
		lcp[rk[i]-1] = h;
	}
}
int pos[N];//pos[x]:x后面第一个lv出现的位置
long long sum = 0;
int idx = 0;
void solve(){
	construct_sa(str,sa);
	construct_lcp(str,sa,lcp);
	pos[n] = n;
	for(int i = n-1;i >= 0;i--)
		if(str[i] == lv[0]) pos[i] = i;
		else pos[i] = pos[i+1];
	sum = n - pos[sa[1]];
	for(int i = 2;i <= n;i++)
		sum += n - max(pos[sa[i]],sa[i]+lcp[i-1]);
	printf("Case #%d: %lld\n",++idx,sum);
}
int main(){
	#ifdef LOCAL
		freopen("123.txt","r",stdin);
		freopen("222.txt","w",stdout);
	#endif
	scanf("%d",&t);
	while(t--){
		scanf("%s%s",lv,str);
		n = strlen(str);
		solve();
	}
	return 0;
}

Life Forms

测试地址
题意简述
给定n个字符串,请问它们之间出现次数超过一半的最长公共子串是什么?如果有多个,按字典序输出。

解题思路
利用后缀数组+高度数组求解。我们将n个字符串拼接中一个“母串”,中间用不同的字符间隔;然后对母串求高度数组lcp,我们依旧是用二分搜索判定长度p是否合法,每次判定从前向后用O(N)时间遍历统计一遍即可。当我们得到最长的合法长度 p 后,再利用类似的方法输出所有的子串。

代码示例

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<iostream>
using namespace std;
string str,s;
int n,k,len;
const int N = 2e5+10;
int sa[N],rk[N],lcp[N],tmp[N];
bool compare_sa(int i,int j){
    if(rk[i] != rk[j]) return rk[i] < rk[j];
    int ri = i+k <= len?rk[i+k]:-1;
    int rj = j+k <= len?rk[j+k]:-1;
    return ri < rj;
}
void construct_sa(string S,int sa[]){
    len = S.length();
    for(int i = 0;i <= len;i++){
        sa[i] = i;
        rk[i] = i < len ? S[i]:-1;
    }
    //k一定不能设置成局部变量导致覆盖全局变量!!!
    for(k = 1;k <= len;k *= 2){
        sort(sa,sa+len+1,compare_sa);
        tmp[sa[0]] = 0;
        for(int i = 1;i <= len;i++)
            tmp[sa[i]] = tmp[sa[i-1]] + compare_sa(sa[i-1],sa[i]);
        for(int i = 0;i <= len;i++) rk[i] = tmp[i];
    }
}
void construct_lcp(string S,int sa[],int lcp[]){
    len = S.length();
    for(int i = 0;i <= len;i++) rk[sa[i]] = i;
    int h = 0; lcp[0] = 0;
    for(int i = 0;i < len;i++){
        int j = sa[rk[i]-1];
        if(h > 0) h--;
        for(;j+h < len && i+h < len;h++)
            if(S[j+h] != S[i+h]) break;
        lcp[rk[i]-1] = h;
    }
}
int vis[110],idx[N];
bool check(int p){
    int cnt = 1; memset(vis,0,sizeof vis);
    vis[idx[sa[1]]] = 1;
    for(int i = 2;i <= len;i++){
        if(lcp[i-1] < p){
            cnt = 1; memset(vis,0,sizeof vis);vis[idx[sa[i]]] = 1;
        }else if(!vis[idx[sa[i]]]) cnt++,vis[idx[sa[i]]] = 1;
        if(cnt >= n/2+1) return true;
    }
    return false;
}
void print(int p){
    int cnt = 1; memset(vis,0,sizeof vis);
    vis[idx[sa[1]]] = 1;
    for(int i = 2;i <= len+1;i++){  //lcp[len+1] = 0,作为结束标志
        if(lcp[i-1] < p){
            if(cnt >= n/2+1) {
                for(int j = sa[i-1];j < sa[i-1]+p;j++) cout << str[j];
                cout << endl;
            }
            cnt = 1; memset(vis,0,sizeof vis);vis[idx[sa[i]]] = 1;
        }else if(!vis[idx[sa[i]]]) cnt++,vis[idx[sa[i]]] = 1;
    }
}
void solve(){
    construct_sa(str,sa);
    construct_lcp(str,sa,lcp);
    /*sa[i]:存放排名第i的后缀的起始位置*/
    int l = 0, r = len;
    while(l <= r){
        int mid = l+r>>1;
        if(check(mid)) l = mid+1;
        else r = mid-1;
    }
    if(r <= 0) cout << "?" << endl;
    else print(r);
}
int main(){
    while(cin >> n && n){
        if(n == 1){
            cin >> str; cout << str << endl << endl;
            continue;
        }
        int cnt = 2,tot = 0; str = "";
        for(int i = 1;i <= n;i++){
            cin >> s; str += s+char(++cnt);
            for(int j = 0;j <= s.length();j++) idx[tot++] = i;
        } 
        solve();
        cout << endl;
    }
    return 0;
}

Maximum repetition substring

题意简述
求一个字符串中,重复次数最多的连续重复子串是什么(循环次数最多的循环子串)。要求输出字典序最小的。

例如:ccabababc中,连续循环次数最多的子串是ababab。

解题思路
这题我对照题解写了很久,思想勉强搞懂。
先说做法,是枚举长度len(从1到n),然后判断以 len 为循环长度的 连续循环次数 最多的 子串 长度 是多少;假设这个子串长度是 l ,那么它的连续循环次数就是 l/len。
我们考虑如何求子串长度 l 。如果有一个字符串的循环节长度为len,那么必然有s[1,n-len] = s[len , n],因此,如果字符串 s[len, n] 与 s[ 2 * len, n] 的公共前缀长度为 lcp,那么 s[len ,n] 的循环节个数为lcp / len + 1(加上的一个是开头为计算在lcp内的循环节 )。
因此我们只需要对s[0] , s[len] ,s[2 * len] … , 判断相差 len 的两个相邻后缀的 lcp,就可以计算该子串循环节个数。这是由于如果一个子串的循环节长度为 len,且循环至少两次,那么显然它会包含相邻的两个len,否则就构不成循环了。当然可能起点并不在len的倍数位置上,因此我们还需要向前拓展一下。

代码示例

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 1e5+10;
char str[N];
int cse = 0,n;
int k,rk[N],lcp[N],sa[N],tmp[N];
bool compare_sa(int i,int j){
	if(rk[i] != rk[j]) return rk[i] < rk[j];
	int ri = i+k <= n?rk[i+k]:-1;
	int rj = j+k <= n?rk[j+k]:-1;
	return ri < rj;
}
void construct_sa(const char* S,int sa[]){
	for(int i = 0;i <= n;i++){
		sa[i] = i;
		rk[i] = i < n?S[i]:-1;
	}
	for(k = 1;k <= n;k *= 2){
		sort(sa,sa+1+n,compare_sa);
		tmp[sa[0]] = 0;
		for(int i = 1;i <= n;i++)
			tmp[sa[i]] = tmp[sa[i-1]]+compare_sa(sa[i-1],sa[i]);
		for(int i = 0;i <= n;i++) rk[i] = tmp[i];
	}
}
void construct_lcp(const char* S,int sa[],int lcp[]){
	for(int i = 0;i <= n;i++) rk[sa[i]] = i;
	int h = 0; lcp[0] = 0;
	for(int i = 0;i < n;i++){
		int j = sa[rk[i]-1];
		if(h > 0) h--;
		for(;j+h < n && i+h < n;h++)
			if(S[j+h] != S[i+h]) break;
		lcp[rk[i]-1] = h;
	}
}
int st[N][22],Log[N];
void st_init(){
	for(int i = 1;i <= n;i++) st[i][0] = lcp[i];
	for(int i = 2;i <= n;i++) Log[i] = Log[i/2]+1;
	for(int j = 1;(1<<j) <= n;j++){
		for(int i = 1;i + (1<<j-1) <= n;i++)
			st[i][j] = min(st[i][j-1],st[i+(1<<j-1)][j-1]);
	}
}
int ask(int l,int r){
	if(l > r) swap(l,r); r--;
	int kk = Log[r-l+1];
	return min(st[l][kk],st[r-(1<<kk)+1][kk]);
}
int mxtc = 0;
int q[N],cnt = 0;
void calc(int len){
	for(int i = 0;i+len < n;i += len){
		int l = ask(rk[i],rk[i+len]);
		int res = l/len+1;
		int pre = i - (len-l%len);
		if(pre >= 0 && ask(rk[pre],rk[pre+len]) >= len) res++;
		if(mxtc < res) mxtc = res,cnt = 0,q[++cnt] = len;
		else if(mxtc == res && len != q[cnt]) q[++cnt] = len;
	}
}
void printAns(){
	for(int i = 1;i <= n;i++)
		for(int j = 1;j <= cnt;j++)
			if(ask(rk[sa[i]],rk[sa[i]+q[j]]) >= q[j]*(mxtc-1)){
				str[sa[i]+q[j]*mxtc] = '\0';
				puts(str+sa[i]); return;
			}
}
void solve(){
	construct_sa(str,sa);
	construct_lcp(str,sa,lcp);//lcp[n]是恒等于0的
	/*下一步求重复次数最多的连续重复子串*/
	st_init();
	mxtc = cnt = 0;
	for(int i = 1;i <= n;i++) calc(i); 
	printf("Case %d: ",++cse);
	printAns();
}
int main(){
	while(scanf("%s",str) != EOF){
		n = strlen(str);
		if(n == 1 && str[0] == '#') break;
		solve();
	}
	return 0;
}

Common Substrings

测试地址
题意简述
给定2个字符串A和B,以及一个整数k。目标是求出这两个字符串中公共子串的数量,例如A:xx , B:xx,k = 1,那么公共子串数量就是5。
解题思路
将A和B拼接在一起,中间用一个未出现过的字符间隔,新串S = A+’#’+B;然后对S求高度数组,利用高度数组来求解。
对于属于 B 的每一个后缀,统计它与所有属于 A 的后缀的 lcp(最长公共前缀),并统计lcp-m+1,这就是该后缀和 A 的公共子串数量。这样做的复杂度是O(N^2)。

我们可以利用单调栈来在 O(N) 时间内解决;我们从前向后遍历lcp数组,将属于A的后缀的 lcp 压入栈;由于两个字符串的后缀是取它们中间的最小值,所以我们应该维护单调递减栈,同时需要维护的是 sum,sum代表当前 A 中相同的子串数量,也就是说如果当前后缀属于 B ,则直接加上sum即可。

由于我们是顺序遍历的,只统计了A对B的贡献,再反过来统计一次B对A的贡献即可。
代码示例

#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
const int N = 2e5+10;
char A[N],B[N];
int rk[N],tmp[N],lcp[N],sa[N],k;
int n,m;
typedef long long ll;
bool compare_sa(int i,int j){
	if(rk[i] != rk[j]) return rk[i] < rk[j];
	int ri = i+k <= n?rk[i+k]:-1;
	int rj = i+k <= n?rk[j+k]:-1;
	return ri < rj;
}
void construct_sa(const char* S,int sa[]){
	for(int i = 0;i <= n;i++){
		sa[i] = i;
		rk[i] = i < n?S[i]:-1;
	}
	for(k = 1;k <= n;k *= 2){
		sort(sa,sa+1+n,compare_sa);
		tmp[sa[0]] = 0;
		for(int i = 1;i <= n;i++)
			tmp[sa[i]] = tmp[sa[i-1]] + compare_sa(sa[i-1],sa[i]);
		for(int i = 0;i <= n;i++) rk[i] = tmp[i];
	}
}
void construct_lcp(const char *S,int sa[],int lcp[]){
	for(int i = 0;i <= n;i++) rk[sa[i]] = i;
	int h = 0; lcp[0] = 0;
	for(int i = 0;i < n;i++){
		int j =sa[rk[i]-1];
		if(h > 0) h--;
		for(;j+h < n && i+h < n;h++)
			if(S[j+h] != S[i+h]) break;
		lcp[rk[i]-1] = h;
	}
}
int idx[N];//初始化为0
struct Node{
	ll h,cnt;
}Stack[N];
void solve(){
	construct_sa(A,sa);
	construct_lcp(A,sa,lcp);
	ll ans = 0,sum = 0;
	int top = 0;
	for(int i = 2;i <= n;i++){
		ll cnt = 0;
		while(top && Stack[top].h >= lcp[i-1]){
			cnt += Stack[top].cnt;
			sum -= Stack[top].cnt * (Stack[top].h-m+1);
			top--;
		}
		if(lcp[i-1] >= m){
			cnt += idx[sa[i-1]] == 0;
			if(cnt) Stack[++top] = Node{lcp[i-1],cnt};
			sum += (lcp[i-1]-m+1)*cnt;
		}
		if(idx[sa[i]] == 1) ans += sum;
	}
	top = 0; sum = 0;
	for(int i = 2;i <= n;i++){
		ll cnt = 0;
		while(top && Stack[top].h >= lcp[i-1]){
			cnt += Stack[top].cnt;
			sum -= Stack[top].cnt * (Stack[top].h-m+1);
			top--;
		}
		if(lcp[i-1] >= m){
			cnt += idx[sa[i-1]] == 1;
			if(cnt) Stack[++top] = Node{lcp[i-1],cnt};
			sum += (lcp[i-1]-m+1)*cnt;
		}
		if(!idx[sa[i]]) ans += sum;
	}
	printf("%lld\n",ans);
}
int main(){
	while(scanf("%d",&m) != EOF && m){
		scanf("%s",A);
		memset(idx,0,sizeof idx);
		n = strlen(A); 
		idx[n] = 2; A[n++] = '#';
		for(int i = 0;i < n;i++) idx[i] = 1;
		scanf("%s",A+n);
		n = strlen(A);
		solve();
	}
	return 0;
}
已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 程序猿惹谁了 设计师:白松林 返回首页