后缀数组学习整理笔记

一、基础概念

char suff[i];//起始位置下标为i 的后缀:下标从i到n的子串
int sa[k];//排名为k的后缀的起始位置下标
int rk[i];//起始位置下标为i 的后缀的排名

int LCP(i,j)  //the LCP of suff[sa[i]] with suff[sa[j]]
//即排名为i与j的后缀的最长公共前缀
# LCP的两个性质
	1.LCP(i,k)=min(LCP(i,j),LCP(j,k))
	其中1<=i<=j<=k<=n
	即可以划分后借用中间值求值
	2.LCP(i,k)=min(LCP(j,j-1))
	其中1<i<j<=k<=n
	即在性质1的基础上,多次划分成相邻后缀求min值

int height[j]=LCP(j,j-1);//排名为 j 与 j-1 的后缀的最长公共前缀长
int h[i]=height[rk[i]];  //j=rk[i]
# h[i]关键性质: h[i]>=h[i-1]-1

对于后缀数组的概念举个例子
	字符串 ababa 
	标号   1~5
那么
	后缀排序为:         首字母下标:
	a					5
	aba					3
	ababa				1
	ba					4
	baba				2

二、模板&题目 A~L

A.后缀排序 后缀数组裸题 来自洛谷P3809
题意:
读入一个长度为 n n n 的由大小写英文字母或数字组成的字符串,请把这个字符串的所有非空后缀按字典序从小到大排序,然后按顺序输出后缀的第一个字符在原串中的位置。位置编号为 1 1 1 n n n
输入:
一行一个长度为 n n n 的仅包含大小写英文字母或数字的字符串
输出:
一行 n n n 个整数
思路:
直接求 s a [ i ] sa[i] sa[i] 按序输出即可
AC代码:

#include<iostream>
#include<cstdio>
#include<algorithm>  
#include<stdlib.h>
#include<cstring>  
#include<cmath>
using namespace std;
#define ll long long
#define N 1000010
using namespace std;
int t1[N],t2[N],sa[N],h[N],rk[N],c[10*N],a[N];
char s[N];
int m,n;

void get_sa(int n,int m)
{
    int *x=t1,*y=t2,p=0,f=0;
    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 i=1;i<=n&&p<=n;i<<=1)
	{
        p=0;
        for(int j=n-i+1;j<=n;j++) y[++p]=j;
        for(int j=1;j<=n;j++) if(sa[j]>i)y[++p]=sa[j]-i;
        for(int j=1;j<=m;j++) c[j]=0;
        for(int j=1;j<=n;j++) c[x[y[j]]]++;
        for(int j=1;j<=m;j++) c[j]+=c[j-1];
        for(int j=n;j;j--) sa[c[x[y[j]]]--]=y[j];
        swap(x,y);
		x[sa[1]]=1;
		p=2;
        for(int j=2;j<=n;j++)
        x[sa[j]]=y[sa[j]]==y[sa[j-1]]&&y[sa[j]+i]==y[sa[j-1]+i]?p-1:p++;
        m=p;
    }
    for(int i=1;i<=n;i++) rk[sa[i]]=i;
    for(int i=1;i<=n;i++)
	{
        int j=sa[rk[i]-1];
        if(f) f--;
		while(a[i+f]==a[j+f]) f++;
        h[rk[i]]=f;
    }
}
int main(){
    scanf("%s",s);
	int len=strlen(s);
    for(int i=0;i<len;i++) a[++n]=s[i]-' ';
    get_sa(n,10000);
    for(int i=1;i<=n;i++) printf("%d ",sa[i]);
    return 0;
}

B. Mr. Panda and Fantastic Beasts
[acm/icpc2016ChinaFinal] F题

题意:
求第一个字符串在其他字符串中未出现过的最短且字典序最小的子串。
思路:
先将所有子串用特殊字符串连接起来, 用后缀数组算出子串的 s a sa sa 数组和 l c p lcp lcp 高度数组,其实就是枚举第一个字符串的每一个子串是否是其他字符串的后缀的前缀即可。
AC代码:

#include<iostream>
#include<cstdio>
#include<algorithm>  
#include<stdlib.h>
#include<cstring>  
#include<cmath>
using namespace std;
typedef long long ll;
const int maxn = 3 * 1e5 + 10;
const ll mod = 1e9 + 7;
using namespace std;
int v = 1;
ll b[maxn], gg[maxn];
int T, n, k, r;
int rk[maxn]; //rk[i] 记录起始位置下标为 i 的后缀的排名 
int sa[maxn]; //sa[k] 记录排名为 k 的后缀的起始位置下标 
int tmp[maxn]; //tmp[n] 间接记录排名,最后用 rk[n] 数组存储 
int  lcp[maxn]; //lcp[k] 记录排名 k 与 k+1 的后缀的最长公共前缀流程 lcp 长度 
string s, t;

bool cmp(int i, int j) {
    if(rk[i]!= rk[j]) return rk[i] < rk[j];//比较起始位置 
    int ri = i + k <= n ? rk[i + k] : -1;  //k是起始下标间隔 
    int rj = j + k <= n ? rk[j + k] : -1;
    return ri < rj;//起始位置相同则比较下一个位置 
}
 
void get_sa(string st)//得到 sa[n] 数组和 rk[n] 数组 
{
    n = st.length();
    for(int i = 0; i <= n; i++) {
        sa[i] = i;
        rk[i] = i < n ? st[i] : -1;
    }
    for(k = 1; k <= n; k *= 2)//倍增 k 
	{
        sort(sa, sa + n + 1, cmp);
        tmp[sa[0]] = 0;//数组从1开始标记,0空缺不用 
        for(int i = 1; i <= n; i++) 
		{
            tmp[sa[i]] = tmp[sa[i - 1]] + (cmp(sa[i - 1], sa[i]) ? 1 : 0);
        }
        for(int i = 0; i <= n; i++) {
            rk[i] = tmp[i];//给后缀按字典序排序 
        }
    }
}
 
void get_lcp(string st) //得到 lcp[n] 数组 
{
    int n = st.length();
    for(int i = 0; i <= n; i++) rk[sa[i]] = i;
    int h = 0;
    lcp[0] = 0;//排名0的lcp置零,无贡献 
    for(int i = 0; i < n; i++ ) 
	{
        int j = sa[rk[i] - 1];
		//j 是起始位置下标为i的后缀的排名前一个后缀的起始下标 
        if(h > 0) h--;
        for(; j + h < n && i + h < n; h++) 
		{
            if(st[j + h] != st[i + h]) break;
			//当找不到公共前缀,则结束循环并记录最大长度 h 
        }
        lcp[rk[i] - 1] = h;
    }
}
 
void solve() 
{
    int ans = r - 1;//记录题意所求的子串的长度 
	int gg = -1;//记录所求子串的起始位置下标 
    for(int i = n; i >= 1; i--)//对字典序前n小的后缀进行判断 
	{
        while(sa[i] >= r) i--;
		//后缀的起始下标大于第一个字符串长度,则肯定不含其子串,跳过 
        if(i < 1) break;
        int res = lcp[i - 1], rs = lcp[i];
        for(int p = i - 1; p && sa[p] < r; p--) {
            res = min(res, lcp[p]);//找min是为了与其他子串公共前缀为0 
        }
        for(int p = i + 1; p <= n && sa[p] < r; p++) {
            rs = min(rs, lcp[p]);
        }
        res = max(res, rs); 
        if(res < r - sa[i]) 
		{//如果公共前缀不为0,则判断其是否超出第一个字符串的范围
            if(res <= ans) 
			{
                ans = res;
                gg = sa[i];
            }
        }
    }
    printf("Case #%d: ", v++);
    if(gg == -1) printf("Impossible\n");
    else
	{
        for(int i = gg; i <= gg + ans; i++) 
		{
            printf("%c", s[i]);
        }
        printf("\n");
    }
}
 
int main() {
    scanf("%d", &T);
    while(T--) 
	{
        scanf("%d", &n);
        s = "";
        cin >> s;//第一个字符串 
        r = s.length();//第一个字符串的长度 r 
        char c = 'z' + 1;//特殊字符用以连接n个字符串 
        for(int i = 1; i < n; i++) {
            cin >> t;
            s += c + t;
        }
        int l = s.length();
        get_sa(s);
        get_lcp(s);
//        for(int i = 1; i <= l; i++) {
//            printf("%d %d\n", sa[i], lcp[i]);
//        }
        solve();
    }
    return 0;
}

C.POJ 2774 后缀数组水题
题意:
求给定两个字符串的的最大公共子串的长度
思路:
用特殊字符将二者连接成一个字符串,然后套模版求 h e i g h t [ i ] height[i] height[i]的最大值,并且要求 s a [ i ] sa[i] sa[i] s a [ i − 1 ] sa[i-1] sa[i1]在特殊字符串两侧即可
AC代码:(似乎这个板子跑得比较快)

#include<iostream>
#include<cstdio>
#include<algorithm>  
#include<stdlib.h>
#include<cstring>  
#include<cmath>
using namespace std;
typedef long long ll;
const int maxn = 1e5 + 10;
const ll mod = 1e9 + 7;
#define min(x,y) x>y? y:x
#define maxn 200010
int dp[maxn][33];
int wa[maxn],wb[maxn],wsf[maxn],wv[maxn],sa[maxn];
int rank[maxn],height[maxn],s[maxn];
char str[maxn],str1[maxn];
int cmp(int *r,int a,int b,int k)
{
    return r[a]==r[b]&&r[a+k]==r[b+k];
}
void getsa(int *r,int *sa,int n,int m)
{
    int i,j,p,*x=wa,*y=wb,*t;
    for(i=0;i<m;i++)  wsf[i]=0;
    for(i=0;i<n;i++)  wsf[x[i]=r[i]]++;
    for(i=1;i<m;i++)  wsf[i]+=wsf[i-1];
    for(i=n-1;i>=0;i--)  sa[--wsf[x[i]]]=i;
    p=1;
    j=1;
    for(;p<n;j*=2,m=p)
    {
        for(p=0,i=n-j;i<n;i++)  y[p++]=i;
        for(i=0;i<n;i++)  if(sa[i]>=j)  y[p++]=sa[i]-j;
        for(i=0;i<n;i++)  wv[i]=x[y[i]];
        for(i=0;i<m;i++)  wsf[i]=0;
        for(i=0;i<n;i++)  wsf[wv[i]]++;
        for(i=1;i<m;i++)  wsf[i]+=wsf[i-1];
        for(i=n-1;i>=0;i--)  sa[--wsf[wv[i]]]=y[i];
        t=x;
        x=y;
        y=t;
        x[sa[0]]=0;
        for(p=1,i=1;i<n;i++)
        x[sa[i]]=cmp(y,sa[i-1],sa[i],j)? p-1:p++;
    }
}
void getheight(int *r,int n)
{
    int i,j,k=0;
    for(i=1;i<=n;i++)  rank[sa[i]]=i;
    for(i=0;i<n;i++)
    {
        if(k)
        k--;
        else
        k=0;
        j=sa[rank[i]-1];
        while(r[i+k]==r[j+k])
        k++;
        height[rank[i]]=k;
    }
}
int main()
{
    while(scanf("%s",str)>0)
    {
        scanf("%s",str1);
        int n=0,len=strlen(str);
        for(int i=0;i<len;i++)
        s[n++]=str[i]-'a'+1;
        s[n++]=28;
        len=strlen(str1);
        for(int i=0;i<len;i++)
        s[n++]=str1[i]-'a'+1;
        s[n]=0;
        getsa(s,sa,n+1,30);
        getheight(s,n);
        int maxx=0,pos=0;
        len=strlen(str);
        for(int i=2;i<n;i++)
        if(height[i]>maxx)
        {
            if(0<=sa[i-1]&&sa[i-1]<len&&len<sa[i])
            maxx=height[i];
            if(0<=sa[i]&&sa[i]<len&&len<sa[i-1])
            maxx=height[i];
        }
        printf("%d\n",maxx);
    }
    return 0;
}[添加链接描述](https://paste.ubuntu.com/p/Zjwz5ZbTBB/)

D. Milk Patterns // POJ 3261
题意:
求可重叠字符串的最长至少 K K K次重叠的子串长
思路:
二分长度再枚举 h e i g h t [ n ] height[n] height[n]数组判断重叠次数
D题AC代码传送门

#include<iostream>
#include<cstdio>
#include<algorithm>  
#include<stdlib.h>
#include<cstring>  
#include<cmath>
using namespace std;
typedef long long ll;
const int maxn = 2e4+ 10;
const ll mod = 1e9 + 7;
#define min(x,y) x>y? y:x
#define maxn 200010
int wa[maxn],wb[maxn],wsf[maxn],wv[maxn];
int rank[maxn],sa[maxn],height[maxn],s[maxn];
int n,k; 

int cmp(int *r,int a,int b,int k)
{
    return r[a]==r[b]&&r[a+k]==r[b+k];
}
void getsa(int *r,int *sa,int n,int m) 
{//对长度小于n且元素值小于m的字符串r求sa[n]数组
    int i,j,p,*x=wa,*y=wb,*t;
    for(i=0;i<m;i++)  wsf[i]=0;
    for(i=0;i<n;i++)  wsf[x[i]=r[i]]++;
    for(i=1;i<m;i++)  wsf[i]+=wsf[i-1];
    for(i=n-1;i>=0;i--)  sa[--wsf[x[i]]]=i;
    p=1;
    j=1;
    for(;p<n;j*=2,m=p)//基数排序更快 
    {
        for(p=0,i=n-j;i<n;i++)  y[p++]=i;
        for(i=0;i<n;i++)  if(sa[i]>=j)  y[p++]=sa[i]-j;
        for(i=0;i<n;i++)  wv[i]=x[y[i]];
        for(i=0;i<m;i++)  wsf[i]=0;
        for(i=0;i<n;i++)  wsf[wv[i]]++;
        for(i=1;i<m;i++)  wsf[i]+=wsf[i-1];
        for(i=n-1;i>=0;i--)  sa[--wsf[wv[i]]]=y[i];
        t=x;
        x=y;
        y=t;
        x[sa[0]]=0;
        for(p=1,i=1;i<n;i++)
        x[sa[i]]=cmp(y,sa[i-1],sa[i],j)? p-1:p++;
    }
}
void getheight(int *r,int n)
{
    int i,j,k=0;
    for(i=1;i<=n;i++)  rank[sa[i]]=i;
    for(i=0;i<n;i++)
    {
        if(k) k--;
        else  k=0;
        j=sa[rank[i]-1];
        while(r[i+k]==r[j+k]) k++;
        height[rank[i]]=k;
    }
}

bool check(int n,int k,int len)
{
	int cnt=1;
	for(int i=2;i<=n;i++)
	{
		if(height[i]>=len)
		{
			cnt++;
			if(cnt>=k) return true;
		}
		else cnt=1;
	}
	return false;
}
int main()
{
    while(scanf("%d%d",&n,&k)!=EOF)
    {
    	int ma=0;
        for(int i=0;i<n;i++)
        {
        	scanf("%d",&s[i]);
        	ma=max(ma,s[i]);
		}
		s[n]=0;
        getsa(s,sa,n+1,ma+1);
        getheight(s,n);
        int l=0,r=n,ans;
        while(l<=r)//二分长度 
        {
        	int mid=(l+r)/2;
        	if(check(n,k,mid)) l=mid+1,ans=mid;
        	else r=mid-1;
		}
        printf("%d\n",ans);
    }
    return 0;
}

E. Life Forms //POJ 3294
题意:
n n n 个字符串,求至少在 k 2 \frac k 2 2k 个串中出现的最长子串
思路:
用不相同的特殊字符将 n n n 个字符组连接后求后缀数组,二分后缀数组判断每组的后缀是否出现在至少 k 2 \frac k 2 2k 个原串中( D D D题的加强版)。
E题AC代码传送门
关键代码:

int check(int n,int k,int mid)
{
	int cnt=0,size=0;
	memset(vis,0,sizeof vis);
	for(int i=1;i<=n;i++)
	{
		if(height[i]>=mid)
		{
			//标记所在字符串,避免重复
			for(int j=1;j<=k;j++)
			{
				if(sa[i]>len[j-1] && sa[i]<len[j]) cnt+=(vis[j]?0:1),vis[j]=1;
				if(sa[i-1]>len[j-1] && sa[i-1]<len[j]) cnt+=(vis[j]?0:1),vis[j]=1;
			}
		}
		else
		{
			if(cnt>k/2) ans[++size]=sa[i-1];
			cnt=0;
			memset(vis,0,sizeof vis);
		}
	}
	if(cnt>k/2) ans[++size]=sa[n];
	if(size)
	{
		ans[0]=size;
		return 1;
	}
	return 0;
}

F.substrings //POJ 1226
题意:
求最长子串长,满足其正向或者反向在全部原串中出现
思路:
依旧连接字符串,二分后缀数组,只是增加每次连接字符串时再连接其反向字符串
AC代码:

#include<iostream>
#include<cstdio>
#include<algorithm>  
#include<stdlib.h>
#include<cstring>  
#include<cmath>
using namespace std;
typedef long long ll;
const int maxn = 2e6+ 10;
const ll mod = 1e9 + 7;
#define min(x,y) x>y? y:x
int wa[maxn],wb[maxn],wsf[maxn],wv[maxn];
int rank[maxn],sa[maxn],height[maxn],s[maxn];
int n,k; 

int cmp(int *r,int a,int b,int k)
{
    return r[a]==r[b]&&r[a+k]==r[b+k];
}
void getsa(int *r,int *sa,int n,int m)//n要包含末尾添加的0
{//对长度小于n且元素值小于m的字符串r求sa[n]数组
    int i,j,p,*x=wa,*y=wb,*t;
    for(i=0;i<m;i++)  wsf[i]=0;
    for(i=0;i<n;i++)  wsf[x[i]=r[i]]++;
    for(i=1;i<m;i++)  wsf[i]+=wsf[i-1];
    for(i=n-1;i>=0;i--)  sa[--wsf[x[i]]]=i;
    p=1;
    j=1;
    for(;p<n;j*=2,m=p)//基数排序更快 
    {
        for(p=0,i=n-j;i<n;i++)  y[p++]=i;
        for(i=0;i<n;i++)  if(sa[i]>=j)  y[p++]=sa[i]-j;
        for(i=0;i<n;i++)  wv[i]=x[y[i]];
        for(i=0;i<m;i++)  wsf[i]=0;
        for(i=0;i<n;i++)  wsf[wv[i]]++;
        for(i=1;i<m;i++)  wsf[i]+=wsf[i-1];
        for(i=n-1;i>=0;i--)  sa[--wsf[wv[i]]]=y[i];
        t=x;
        x=y;
        y=t;
        x[sa[0]]=0;
        for(p=1,i=1;i<n;i++)
            x[sa[i]]=cmp(y,sa[i-1],sa[i],j)? p-1:p++;
    }
}
void getheight(int *r,int n)//n不保存最后的0
{
    int k=0;
    for(int i=1;i<=n;i++)  rank[sa[i]]=i;
    for(int i=0;i<n;i++)
    {
        if(k) k--;
        else  k=0;
        int j=sa[rank[i]-1];
        while(r[i+k]==r[j+k]) k++;
        height[rank[i]]=k;
    }
}

char str[maxn];
int ans[maxn],id[maxn];
bool vis[105];
 
bool check(int n,int k,int mid)
{
    int size = 0,cnt = 0;
    memset(vis,false,sizeof vis);
    for(int i = 1; i<=n; i++)
    {
        if(height[i]>=mid)
        {
            for(int j = 0; j<k; j++)
            {
              if(id[sa[i]]==j) cnt+=(vis[j]?0:1),vis[j]=true;
              if(id[sa[i-1]]==j) cnt+=(vis[j]?0:1),vis[j]=true;
            }
        }
        else
        {
            if(cnt>=k) return true;
            cnt = 0;
            memset(vis,false,sizeof vis);
        }
    }
    if(cnt>=k) return true;
    return false;
}


int main()
{
	int t;
	scanf("%d",&t);
    while(t--)
    {
    	scanf("%d",&k);
    	int n=0;
		int b=1;
        for(int i=0;i<k;i++)
        {
        	scanf("%s",str);
        	int len=strlen(str);
        	for(int j=0;j<len;j++)
        	{
        		id[n]=i;//标记某个位置的字符属于第几个字符串 
        		s[n++]=str[j];
			}
			s[n++]='#'+(b++);//连接字符 
			for(int j=len-1;j>=0;j--)//逆序 
			{
				id[n]=i;
				s[n++]=str[j];
			}
			s[n++]='#'+(b++);
		}
		s[n-1]=0;
        getsa(s,sa,n,255);
        getheight(s,n-1);
        int l=1,r=n,ans=0;
        while(l<=r)//二分长度 
        {
        	int mid=(l+r)/2;
        	if(check(n,k,mid)) ans=mid,l=mid+1;
        	else r=mid-1;
		}
       printf("%d\n",ans);
    }
    return 0;
}

G. Musical Theme //POJ 1743
题意:
给一个 n n n长的串,求重复出现(整个子串相差常数值也算重复)但不重叠的最长子串长度。(要求长度至少为5)
思路:
二分枚举后缀长度 k k k,要求 h e i g h t [ i ] &gt; k height[i]&gt;k height[i]>k m a x ( s a [ i ] − s a [ j ] ) &gt; k max(sa[i]-sa[j])&gt;k max(sa[i]sa[j])>k
AC代码:

#include<iostream>
#include<cstdio>
#include<algorithm>  
#include<stdlib.h>
#include<cstring>  
#include<cmath>
using namespace std;
typedef long long ll;
const int maxn = 2e6+ 10;
const ll mod = 1e9 + 7;
#define min(x,y) x>y? y:x
int wa[maxn],wb[maxn],wsf[maxn],wv[maxn];
int rank[maxn],sa[maxn],height[maxn],s[maxn];
int n,k; 

int cmp(int *r,int a,int b,int k)
{
    return r[a]==r[b]&&r[a+k]==r[b+k];
}
void getsa(int *r,int *sa,int n,int m)//n要包含末尾添加的0
{//对长度小于n且元素值小于m的字符串r求sa[n]数组
    int i,j,p,*x=wa,*y=wb,*t;
    for(i=0;i<m;i++)  wsf[i]=0;
    for(i=0;i<n;i++)  wsf[x[i]=r[i]]++;
    for(i=1;i<m;i++)  wsf[i]+=wsf[i-1];
    for(i=n-1;i>=0;i--)  sa[--wsf[x[i]]]=i;
    p=1;
    j=1;
    for(;p<n;j*=2,m=p)//基数排序更快 
    {
        for(p=0,i=n-j;i<n;i++)  y[p++]=i;
        for(i=0;i<n;i++)  if(sa[i]>=j)  y[p++]=sa[i]-j;
        for(i=0;i<n;i++)  wv[i]=x[y[i]];
        for(i=0;i<m;i++)  wsf[i]=0;
        for(i=0;i<n;i++)  wsf[wv[i]]++;
        for(i=1;i<m;i++)  wsf[i]+=wsf[i-1];
        for(i=n-1;i>=0;i--)  sa[--wsf[wv[i]]]=y[i];
        t=x;
        x=y;
        y=t;
        x[sa[0]]=0;
        for(p=1,i=1;i<n;i++)
            x[sa[i]]=cmp(y,sa[i-1],sa[i],j)? p-1:p++;
    }
}
void getheight(int *r,int n)//n不保存最后的0
{
    int k=0;
    for(int i=1;i<=n;i++)  rank[sa[i]]=i;
    for(int i=0;i<n;i++)
    {
        if(k) k--;
        else  k=0;
        int j=sa[rank[i]-1];
        while(r[i+k]==r[j+k]) k++;
        height[rank[i]]=k;
    }
}
 
bool check(int mid)
{
    int maxn=sa[1];
    int minn=sa[1];
    for(int i = 2; i < n; i++)
    {
        if(height[i]>=mid && i<n-1)
        {
            maxn=max(maxn,sa[i]);
            minn=min(minn,sa[i]);
            continue;
        }
        if(maxn-minn>=mid) return true;
        maxn=minn=sa[i];
    }
    return false;
}

int main()
{
    while(~scanf("%d",&n),n)
    {
        for(int i=0;i<n;i++)
        {
        	scanf("%d",&s[i]);
		}
		for(int i=0;i<n-1;i++)
		{
			s[i]=s[i+1]-s[i]+100;
		}
		s[n-1]=0;
        getsa(s,sa,n,255);
        getheight(s,n-1);
        int l=4,r=n-1,ans=0;
        while(l<=r)//二分长度 
        {
        	int mid=(l+r)/2;
        	if(check(mid)) ans=mid,l=mid+1;
        	else r=mid-1;
		}
		ans++;
        printf("%d\n",ans<5?0:ans);
    }
    return 0;
}

H. Longest Repeated subsequence //HDU 2890
题意:
D D D ( P O J 3261 ) (POJ 3261) (POJ3261)的基础上加了子串不可重叠的要求,即求不可重叠的字符串的最长至少 K K K次重叠的子串长和字典序最小的该子串。
传送门>参考这个博客

I. New Distinct Substrings //SPOJ 705
题意:
求一个字符串的不同的子串的个数
思路:
板子题,关键在于推导出下面这个式子: a n s = ∑ ( n − s a [ i ] − h e i g h t [ i ] ) ans=\sum{(n-sa[i]-height[i])} ans=(nsa[i]height[i])
I题AC代码

//主函数用式子求和即可,其余部分套板子就完事了
int main() {
    scanf("%d", &T);
    while(T--) 
	{
        s = "";
        cin >> s;  
        int l = s.length();
        get_sa(s);
        get_lcp(s);
        int ans=0;
        for(int i = 1; i <= l; i++) {
           ans+=n-sa[i]-lcp[i];//推导式子
        }
        printf("%d\n",ans);
    }
    return 0;
}

J. Repeats // SPOJ 687(论文题)
题意:
给定长度 n n n的字符串 s s s, 定义 ( k , l ) (k,l) (k,l)子串为 k k k l l l长循环节组成的字符串, 求 s s s的最大 k k k
思路:
论文中分析如下:
论文的分析
往后匹配只需求两个后缀的 L C P LCP LCP
往前匹配的话只需加一个 L L L长度判断 L C P LCP LCP是否增加一个循环节。
(因为是二分长度判断,所以如果加两个L的答案已经枚举过)
J题AC代码:

#include<iostream>
#include<cstdio>
#include<algorithm>  
#include<stdlib.h>
#include<cstring>  
#include<cmath>
using namespace std;
typedef long long ll;
const int maxn = 3 * 1e5 + 10;
const ll mod = 1e9 + 7;
using namespace std;
int T, n, k, r;
int rk[maxn]; //rk[i] 记录起始位置下标为 i 的后缀的排名 
int sa[maxn]; //sa[k] 记录排名为 k 的后缀的起始位置下标 
int tmp[maxn]; //tmp[n] 间接记录排名,最后用 rk[n] 数组存储 
int  lcp[maxn]; //lcp[k] 记录排名 k 与 k+1 的后缀的最长公共前缀流程 lcp 长度 
string s, t;
int dp[maxn][20];//

bool cmp(int i, int j) {
    if(rk[i]!= rk[j]) return rk[i] < rk[j];//比较起始位置 
    int ri = i + k <= n ? rk[i + k] : -1;  //k是起始下标间隔 
    int rj = j + k <= n ? rk[j + k] : -1;
    return ri < rj;//起始位置相同则比较下一个位置 
}
 
void get_sa(string st)//得到 sa[n] 数组和 rk[n] 数组 
{
    n = st.length();
    for(int i = 0; i <= n; i++) {
        sa[i] = i;
        rk[i] = i < n ? st[i] : -1;
    }
    for(k = 1; k <= n; k *= 2)//倍增 k 
	{
        sort(sa, sa + n + 1, cmp);
        tmp[sa[0]] = 0;//数组从1开始标记,0空缺不用 
        for(int i = 1; i <= n; i++) 
		{
            tmp[sa[i]] = tmp[sa[i - 1]] + (cmp(sa[i - 1], sa[i]) ? 1 : 0);
        }
        for(int i = 0; i <= n; i++) {
            rk[i] = tmp[i];//给后缀按字典序排序 
        }
    }
}
 
void get_lcp(string st) //得到 lcp[n] 数组 
{
    int n = st.length();
    for(int i = 0; i <= n; i++) rk[sa[i]] = i;
    int h = 0;
    lcp[0] = 0;//排名0的lcp置零,无贡献 
    for(int i = 0; i < n; i++ ) 
	{
        int j = sa[rk[i] - 1];
		//j 是起始位置下标为i的后缀的排名前一个后缀的起始下标 
        if(h > 0) h--;
        for(; j + h < n && i + h < n; h++) 
		{
            if(st[j + h] != st[i + h]) break;
			//当找不到公共前缀,则结束循环并记录最大长度 h 
        }
        lcp[rk[i] - 1] = h;
    }
}

int qrmq(int l,int r)
{//通过rk[l]到rk[r]之间的 min(lcp[i])求起始下标为 l,r 的后缀的最长公共前缀
	int a=rk[l];
	int b=rk[r];
	if(a>b) swap(a,b);a++;
	int k=0;
	while((1<<(k+1))<=b-a+1) k++;
	int ant=min(dp[a][k],dp[b-(1<<k)+1][k]);
	return ant;
}
 
int main() {
    scanf("%d", &T);
    while(T--) 
	{
		scanf("%d",&n);
		s = "";
		char c;
		for(int i=0;i<n;i++)
		{
			cin>>c;
			s+=c;
	    }   
        //int l = s.length();
        get_sa(s);
        get_lcp(s);
        for(int i=1;i<=n;i++){
        	dp[i][0]=lcp[i-1];
		}
		for(int i=1;(1<<i)<=n;i++){
			for(int j=1;j+(1<<i)-1<=n;j++){
				dp[j][i]=min(dp[j][i-1],dp[j+(1<<(i-1))][i-1]);
			}
		}
        int ans=0,sum;
        for(int len = 1; len <= n; len++) {//枚举循环节长度 
           for(int i=0;i+len<n;i+=len){
           	 int t=qrmq(i,i+len);
           	 sum=t/len+1;
           	 int pos=i-(len-t%len);
           	 if(pos>=0 && t%len!=0){
           	 	if(qrmq(pos,pos+len)>=(len-t%len)) sum++;
			 }
			 if(sum>ans) ans=sum;
		   }
        }
        printf("%d\n",ans);
    }
    return 0;
}

K. Extend to Palindrome // Uva 11475
题意:
在给定 n n n长字符串后面添加尽可能少的字符使之成为回文串。
思路:
此题虽说后缀数组也能做,但是 K M P KMP KMP做法更优,这里仅给出 K M P KMP KMP的代码。
后缀数组+ST表做法
KMP做法AC代码:

#include<iostream>
#include<cstdio>
#include<algorithm>  
#include<stdlib.h>
#include<cstring>  
#include<cmath>
using namespace std;
typedef long long ll;
const int maxn = 1e5 + 10;
char strA[maxn];
char strB[maxn];
int  next_[maxn];
 
void getnext(char T[])  
{  
    next_[0] = -1;
    int i = 0, j = -1;  
    while (T[i]) {  
        if (j == -1 || T[i] == T[j]) {
			++ i; 
			++ j;
			if (T[i] != T[j]) next_[i] = j;
            else next_[i] = next_[j];  
        }
		else j = next_[j];  
    }
}  
 
int KMP(char S[], char T[])
{
	int i = 0, j = 0;
	while (S[i]) {
		if (j == -1 || S[i] == T[j]) {
			i ++;
			j ++;
		}
		else j = next_[j];
	}
	return j;
}

int main()
{
	while (~scanf("%s",strA))
	{
		int len = strlen(strA);
		for (int i = 0; i < len; ++ i)
			strB[i] = strA[len-1-i];
		strB[len] = 0;
		getnext(strB);
		printf("%s%s\n",strA,&strB[KMP(strA, strB)]);
	}
    return 0;
}

L. Common Substrings //POJ 3415 需用单调栈优化
题意: 给定两个字符串 A , B A,B A,B,求长度至少为 K K K的公共子串的个数。
思路:
A A A, B B B的各个后缀的贡献 l c p − k + 1 lcp-k+1 lcpk+1 进行求和。
首先将两个字符串用特殊字符连接,划分 h e i g h t height height数组,求一个组中一个A串与之前B串形成的 L C P LCP LCP的贡献 ( l c p − k + 1 ) (lcp-k+1) (lcpk+1)和一个 B B B串与之前 A A A串形成的 L C P LCP LCP的贡献。用一个单调栈,栈中存放两个元素分别 h e i g h t t o p height_{top} heighttop c n t t o p cnt_{top} cnttop ,分别表示到 i i i 为止的最小 h e i g h t height height 和子串的数目。维护栈中元素的 h e i g h t height height 从栈顶到栈底递减:每加入一个元素如果该元素比栈顶元素小则需要将 t o t tot tot c n t t o p cnt_{top} cnttop 个已经累计的 h e i g h t t o p height_{top} heighttop 全部替换为当前元素的 h e i g h t height height ( l c p lcp lcp是取区间最小值)。
L题AC代码:

#include<iostream>
#include<cstdio>
#include<algorithm>  
#include<stdlib.h>
#include<cstring>  
#include<cmath>
using namespace std;
typedef long long ll;
const int maxn = 2e5+ 10;
const ll mod = 1e9 + 7;
int wa[maxn],wb[maxn],wsf[maxn],wv[maxn];
int rank[maxn],sa[maxn],height[maxn];
int n,k; 

int cmp(int *r,int a,int b,int k)
{
    return r[a]==r[b]&&r[a+k]==r[b+k];
}
void getsa(char *r,int *sa,int n,int m)//n要包含末尾添加的0
{//对长度小于n且元素值小于m的字符串r求sa[n]数组
    int i,j,p,*x=wa,*y=wb,*t;
    for(i=0;i<m;i++)  wsf[i]=0;
    for(i=0;i<n;i++)  wsf[x[i]=r[i]]++;
    for(i=1;i<m;i++)  wsf[i]+=wsf[i-1];
    for(i=n-1;i>=0;i--)  sa[--wsf[x[i]]]=i;
    p=1;
    j=1;
    for(;p<n;j*=2,m=p)//基数排序更快 
    {
        for(p=0,i=n-j;i<n;i++)  y[p++]=i;
        for(i=0;i<n;i++)  if(sa[i]>=j)  y[p++]=sa[i]-j;
        for(i=0;i<n;i++)  wv[i]=x[y[i]];
        for(i=0;i<m;i++)  wsf[i]=0;
        for(i=0;i<n;i++)  wsf[wv[i]]++;
        for(i=1;i<m;i++)  wsf[i]+=wsf[i-1];
        for(i=n-1;i>=0;i--)  sa[--wsf[wv[i]]]=y[i];
        t=x;
        x=y;
        y=t;
        x[sa[0]]=0;
        for(p=1,i=1;i<n;i++)
            x[sa[i]]=cmp(y,sa[i-1],sa[i],j)? p-1:p++;
    }
}
void getheight(char *r,int n)//n不保存最后的0
{
    int k=0;
    for(int i=1;i<=n;i++)  rank[sa[i]]=i;
    for(int i=0;i<n;i++)
    {
        if(k) k--;
        else  k=0;
        int j=sa[rank[i]-1];
        while(r[i+k]==r[j+k]) k++;
        height[rank[i]]=k;
    }
}

char str[maxn],tt[maxn];
int sta[maxn][2];
//sta[i][0]为单调栈中存储的LCP的长度,sta[i][1]为该lcp的重复次数 
ll tot,top;

int main(){
	while(scanf("%d",&k)!=EOF&&k)
	{
		scanf("%s%s",str,tt);
		int l1=strlen(str);
		int l2=strlen(tt);
		str[l1]='#';
		int n=l1+l2+1;
		for(int i=l1+1;i<n;i++) str[i]=tt[i-l1-1];
		str[n]='\0';
		getsa(str,sa,n+1,255);
		getheight(str,n);
		
		ll sum=0;
		tot=top=0;
		for(int i=1;i<=n;i++)//从前串找后串 
        {
        	if(height[i]>=k)
        	{
        		int cnt=0;
        		if(sa[i-1]<l1) cnt++,tot+=height[i]-k+1;
        		while(top && height[i]<=sta[top-1][0]){
        			top--;
        			tot-=sta[top][1]*(sta[top][0]-height[i]);
        			cnt+=sta[top][1];
				}
				sta[top][0]=height[i];
				sta[top][1]=cnt;
				top++;
				if(sa[i]>l1) sum+=tot;
			}
			else tot=top=0;
		}
		
		tot=top=0;
		for(int i=1;i<=n;i++)//从后串找前串 
        {
        	if(height[i]>=k)
        	{
        		int cnt=0;
        		if(sa[i-1]>l1) cnt++,tot+=height[i]-k+1;
        		while(top && height[i]<=sta[top-1][0]){
        			top--;
        			tot-=sta[top][1]*(sta[top][0]-height[i]);
        			cnt+=sta[top][1];
				}
				sta[top][0]=height[i];
				sta[top][1]=cnt;
				top++;
				if(sa[i]<l1) sum+=tot;
			}
			else tot=top=0;
		}
		
		printf("%I64d\n",sum);
	}
	return 0;
}

这篇文章有点长(啰嗦),能看到这里的都是有恒心毅力的,辛苦啦!
其实是想通过对比不同的变形题加深对后缀数组的理解和应用,如果能够融汇贯通的话就可以应对大多的后缀数组题了~~

路漫漫其修远兮,吾将上下而求索。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值