( 字符串专题 )【 后缀数组 】
推荐阅读:https://www.cnblogs.com/zwfymqz/p/8413523.html
倍增法代码模板:
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e6+10;
char s[maxn];
int n,M,rak[maxn],sa[maxn],tax[maxn],tp[maxn],height[maxn];
void Qsort() // 基数排序
{
for ( int i=0; i<=M; i++ ) tax[i] = 0;
for ( int i=1; i<=n; i++ ) tax[rak[i]] ++;
for ( int i=1; i<=M; i++ ) tax[i] += tax[i-1];
for ( int i=n; i>=1; i-- ) sa[ tax[rak[tp[i]]]-- ] = tp[i];
}
void get_sa()
{
M = 75; // 固定值,和s[i]的范围有关, 如果s[i]最大是1e5, M 也设置成1e5
for ( int i=1; i<=n; i++ ) {
rak[i] = s[i] - '0' + 1;
tp[i] = i;
}
Qsort();
for ( int w=1,p=0; p<n; M=p,w*=2 ) {
//w:当前倍增的长度,w = x表示已经求出了长度为x的后缀的排名,现在要更新长度为2x的后缀的排名
//p表示不同的后缀的个数,很显然原字符串的后缀都是不同的,因此p = N时可以退出循环
p = 0; //这里的p仅仅是一个计数器000
for ( int i=1; i<=w; i++ ) tp[++p] = n-w+i; //前w名是后缀长度不足2*w的后缀
for ( int i=1; i<=n; i++ ) if ( sa[i]>w ) tp[++p]=sa[i]-w; // 舍去长度不足的,按sa顺序存入
Qsort();//此时我们已经更新出了第二关键字,利用上一轮的rak更新本轮的sa
std::swap(tp,rak); //这里原本tp已经没有用了,tp变成上一轮的rak,方便下面更新新的rak
rak[sa[1]] = p = 1;
for ( int i=2; i<=n; i++ ) { // 更新rak值
rak[sa[i]] = ( tp[sa[i-1]]==tp[sa[i]]&&tp[sa[i-1]+w]==tp[sa[i]+w] )?p:++p;
} //这里当两个后缀上一轮排名相同时本轮也相同,两个后缀的名次一定是相同的
}
}
void get_hight() // height[i] = 排名为 i 的后缀与排名为 i−1 的后缀的最长公共前缀
{ // height[i] = lcp( sa[i], sa[i-1] );
int j,k=0;
for ( int i=1; i<=n; i++ ) {
if ( k ) k--;
int j = sa[rak[i]-1];
while ( s[i+k]==s[j+k] ) k++;
height[rak[i]] = k;
}
}
int main()
{
cin >> s+1 ; // 下标从1开始
n = strlen(s+1);
get_sa();
for ( int i=1; i<=n; i++ ) {
cout << sa[i] << " ";
}
return 0;
}
dc3法模板
/*
之前一直用倍增法,发现有些题目卡倍增法,而DC3却能AC,所以顺便弄了
DC3的模版,看以后会不会用到,嗯,就是酱紫
提一些注意点:1.MAXN开n的十倍大小;
2.dc3(r,sa,n+1,Max+1);r为待后缀处理的数组,sa为存储排名位置的数组,n+1和Max+1 都和倍增一样
3.calheight(r,sa,n);和倍增一样
模版测试题目是 SPOJ 694 / SPOJ DISUBSTR Distinct Substrings【后缀数组】不相同的子串的个数
做法和我之前写的那篇题解一样,大概就这些..
*/
#include<cstdio>
#include<algorithm>
#include<queue>
#include<iostream>
#include<cmath>
#include<cstring>
using namespace std;
#define F(x) ((x)/3+((x)%3==1?0:tb))
#define G(x) ((x)<tb?(x)*3+1:((x)-tb)*3+2)
const int MAXN = 10000+100;//n*10
int sa[MAXN];
int rank[MAXN];
int height[MAXN];
int n;
char s[MAXN];
int r[MAXN];
int wa[MAXN],wb[MAXN],wv[MAXN];
int wws[MAXN];
void sort(int *r,int *a,int *b,int n,int m)
{
int i;
for(i=0;i<n;i++) wv[i]=r[a[i]];
for(i=0;i<m;i++) wws[i]=0;
for(i=0;i<n;i++) wws[wv[i]]++;
for(i=1;i<m;i++) wws[i]+=wws[i-1];
for(i=n-1;i>=0;i--) b[--wws[wv[i]]]=a[i];
return;
}
int c0(int *r,int a,int b)
{return r[a]==r[b]&&r[a+1]==r[b+1]&&r[a+2]==r[b+2];}
int c12(int k,int *r,int a,int b)
{if(k==2) return r[a]<r[b]||r[a]==r[b]&&c12(1,r,a+1,b+1);
else return r[a]<r[b]||r[a]==r[b]&&wv[a+1]<wv[b+1];}
void dc3(int *r,int *sa,int n,int m)
{
int i,j,*rn=r+n,*san=sa+n,ta=0,tb=(n+1)/3,tbc=0,p;
r[n]=r[n+1]=0;
for(i=0;i<n;i++) if(i%3!=0) wa[tbc++]=i;
sort(r+2,wa,wb,tbc,m);
sort(r+1,wb,wa,tbc,m);
sort(r,wa,wb,tbc,m);
for(p=1,rn[F(wb[0])]=0,i=1;i<tbc;i++)
rn[F(wb[i])]=c0(r,wb[i-1],wb[i])?p-1:p++;
if(p<tbc) dc3(rn,san,tbc,p);
else for(i=0;i<tbc;i++) san[rn[i]]=i;
for(i=0;i<tbc;i++) if(san[i]<tb) wb[ta++]=san[i]*3;
if(n%3==1) wb[ta++]=n-1;
sort(r,wb,wa,ta,m);
for(i=0;i<tbc;i++) wv[wb[i]=G(san[i])]=i;
for(i=0,j=0,p=0;i<ta && j<tbc;p++)
sa[p]=c12(wb[j]%3,r,wa[i],wb[j])?wa[i++]:wb[j++];
for(;i<ta;p++) sa[p]=wa[i++];
for(;j<tbc;p++) sa[p]=wb[j++];
return;
}
void calheight(int *r, int *sa, int n)
{
int i, j, k = 0;
for (i = 1; i <= n; ++i) rank[sa[i]] = i;
for (i = 0; i < n; height[rank[i++]] = k)
for (k ? k-- : 0, j = sa[rank[i] - 1]; r[i + k] == r[j + k]; ++k);
return;
}
int main()
{
int t;
scanf("%d",&t);
while(t--){
scanf("%s",s);
int Max=-1;
n=strlen(s);
for(int i=0;i<n;i++){
r[i]=s[i];
if(r[i]>Max)Max=r[i];
}
r[n]=0;
dc3(r,sa,n+1,Max+1);
calheight(r,sa,n);
int sum=0;
for(int i=2;i<=n;i++)sum+=height[i];
printf("%d\n",(1+n)*n/2-sum);
}
return 0;
}
例题1 :A - Musical Theme POJ - 1743 ( 不可重叠最长重复子串 )
题意:有N(1 <= N <=20000)个音符的序列来表示一首乐曲,每个音符都是1..88范围内的整数,现在要找一个重复的主题。
“主题”是整个音符序列的一个子串,它需要满足如下条件:
1.长度至少为5个音符
2.在乐曲中重复出现(可能经过转调,“转调”的意思是主题序列中每个音符都被加上或减去了同一个整数值。)
3.重复出现的同一主题不能有公共部分。
有个非常巧妙的思路:
- 首先把问题转化成重复子串的问题:把原串每一位都与前一位相减。这样得出的新串如果有两个长度为n的子串相同,那么它们对应在原串的长度n+1的子串也就相似。
大概描述一下不可重叠最长重复子串的解法:
O(logn)二分枚举子串长度,判断解是否成立
O(n)判断长度是否成立:把互相之间LCP大于等于长度的分为一组,这通过个扫一遍height即可,因为后缀是有序的,相邻的后缀间的LCP必定的极大的;接下来就找到每个组里后缀sa值最大和最小的,如果差值大于(等于)k就成立,因为这样小下标的后缀沿着LCP下去走k步才不会盖到大下标的后缀。
代码:
#include <iostream>
#include <stdio.h>
using namespace std;
const int maxn = 1e6+10;
int s[maxn];
int n,M,rak[maxn],sa[maxn],tax[maxn],tp[maxn],height[maxn];
void Qsort() // 基数排序
{
for ( int i=0; i<=M; i++ ) tax[i] = 0;
for ( int i=1; i<=n; i++ ) tax[rak[i]] ++;
for ( int i=1; i<=M; i++ ) tax[i] += tax[i-1];
for ( int i=n; i>=1; i-- ) sa[ tax[rak[tp[i]]]-- ] = tp[i];
}
void get_sa()
{
M = 400; // 固定值, 和下面的s[i]-‘0’+1 对应
for ( int i=1; i<=n; i++ ) {
rak[i] = s[i] ;
tp[i] = i;
}
Qsort();
for ( int w=1,p=0; p<n; M=p,w*=2 ) {
//w:当前倍增的长度,w = x表示已经求出了长度为x的后缀的排名,现在要更新长度为2x的后缀的排名
//p表示不同的后缀的个数,很显然原字符串的后缀都是不同的,因此p = N时可以退出循环
p = 0; //这里的p仅仅是一个计数器000
for ( int i=1; i<=w; i++ ) tp[++p] = n-w+i; //前w名是后缀长度不足2*w的后缀
for ( int i=1; i<=n; i++ ) if ( sa[i]>w ) tp[++p]=sa[i]-w; // 舍去长度不足的,按sa顺序存入
Qsort();//此时我们已经更新出了第二关键字,利用上一轮的rak更新本轮的sa
std::swap(tp,rak); //这里原本tp已经没有用了,tp变成上一轮的rak,方便下面更新新的rak
rak[sa[1]] = p = 1;
for ( int i=2; i<=n; i++ ) { // 更新rak值
rak[sa[i]] = ( tp[sa[i-1]]==tp[sa[i]]&&tp[sa[i-1]+w]==tp[sa[i]+w] )?p:++p;
} //这里当两个后缀上一轮排名相同时本轮也相同,两个后缀的名次一定是相同的
}
}
void get_hight() // height[i] = 排名为 i 的后缀与排名为 i−1 的后缀的最长公共前缀
{ // height[i] = lcp( sa[i], sa[i-1] );
int j,k=0;
for ( int i=1; i<=n; i++ ) {
if ( k ) k--;
int j = sa[rak[i]-1];
while ( s[i+k]==s[j+k] ) k++;
height[rak[i]] = k;
}
}
int solve( int x )
{
int maxx=-1, minn=0x3f3f3f3f;
for ( int i=2; i<=n; i++ ) {
if ( height[i]>=x ) {
int pos1 = sa[i]; // 根据height数组定义
int pos2 = sa[i-1]; // 对应了两个后缀字串,取最值
maxx = max(maxx,max(pos1,pos2));
minn = min(minn,min(pos1,pos2));
if ( maxx-minn>=x ) { // 后缀字串的位置差了x, 所以一定不重叠
return 1;
}
}
else {
maxx=-1;
minn=0x3f3f3f3f;
}
}
return 0;
}
int main()
{
while ( ~scanf("%d",&n) && n ) {
for ( int i=1; i<=n; i++ ) {
scanf("%d",&s[i]);
}
n --;
for ( int i=1; i<=n; i++ ) {
s[i] = s[i]-s[i+1] + 90; // 求差值数列
}
get_sa();
get_hight();
int ans = 0;
int left = 4, right=20005;
while ( left<=right ) {
int mid = (left+right)/2;
if ( solve(mid)==1 ) {
ans = mid;
left = mid + 1;
}
else {
right = mid - 1;
}
}
if ( ans<4 ) {
printf("0\n");
}
else printf("%d\n",ans+1);
}
return 0;
}
例题2 : B - Milk Patterns POJ - 3261 ( 重复k次的最长字串 )
题意:找出出现k次的可重叠的最长子串的长度
思路:用后缀数组求出lcp后,2分枚举L使得连续的lcp[i]>=L 的个数>=k-1;
代码:
void get_sa()
{
M = 100005; // M
}
int solve( int x )
{
int cnt=1 ;
for ( int i=2; i<=n; i++ ) {
if ( height[i]>=x ) {
cnt ++;
}
else {
if ( cnt>=k ) return 1;
cnt = 1;
}
}
if ( cnt>=k ) return 1;
return 0;
}
int main()
{
cin >>n >> k;
for ( int i=1; i<=n; i++ ) {
scanf("%d",&s[i]);
}
get_sa();
get_hight();
int left = 0, right = 20005;
int ans=0;
while ( left<=right ) {
int mid = (left+right)/2;
if ( solve(mid)==1 ) {
ans = mid;
left = mid+1;
}
else {
right = mid-1;
}
}
cout << ans << endl;
return 0;
}
例题3 :C - Distinct Substrings SPOJ - DISUBSTR ( 不同子串个数 )
题意:求不同子串的个数
思路:长度为n的字符串有n*(n+1)/2个子串,再减去相同的子串就行了
对于子串,它肯定是一个后缀的前缀,如果height[i]==k,说明后缀i-1和后缀i有k个子串相同,
这样减去它即可,即减去height数组的和即可
int main()
{
int listt;
cin >> listt;
while ( listt-- ) {
cin >> s+1 ; // 下标从1开始
n = strlen(s+1);
get_sa();
get_hight();
int ans = n*(n+1)/2;
for ( int i=2; i<=n; i++ ) {
ans -= height[i];
}
cout << ans << endl;
}
return 0;
}
例题4 :