( 字符串专题 )【 后缀数组 】

( 字符串专题 )【 后缀数组 】

推荐阅读: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 :

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值