后缀数组总结(题目合集)

1、hdu6704 K-th occurrence(主席树+后缀树组)

题意:给定串s, q个询问(l,r,k),求子串s[l,r]的第k次出现位置。

分析:好题。考虑后缀树组sa的含义,sa数组将后缀排序之后,前缀最相似的必然放在了一起(相邻),如果sa[x]前后能找到和后缀x的最长公共前缀大于等于区间长度的后缀,那么说明有子串重复,我们在sa数组找到满足情况(lcp(i,x)>=len)的左右边界(l、r),如果r-l+1>=k,说明存在k个重复的子串,sa[l]-sa[r]存放就是他们出现的位置,第k大就是第k次出现的位置(首字符)。二分上下界要注意二分的姿势和细节,很容易出错。

总结:被这个题目整了一天,对后缀树组的理解深入了不少,后缀树组还是很强大的,也复习了一下主席树,现在看来就是个模板题,对模板一定要熟悉,一定要用自己的,不然会吃大亏,以后都用这个题目的代码当模板了。

代码:

#include <bits/stdc++.h>
using namespace std;
const int N=1e5+5;
char s[N];
int y[N],x[N],c[N],sa[N],rk[N],height[N];
int t,n,m,q;

/// -- 后缀树组模板部分 get_SA + get_height + bd_st() + rmq + qu_lcp 五个函数 --
/// 字符串下标从1开始,因为主席树需要前缀和做差
void get_SA() {
    memset(c,0,sizeof(c));
    for (int i=1; i<=n; ++i) ++c[x[i]=s[i]];
    for (int i=2; i<=m; ++i) c[i]+=c[i-1];
    for (int i=n; i>=1; --i) sa[c[x[i]]--]=i;
    for (int k=1; k<=n; k<<=1) {
        int num=0;
        for (int i=n-k+1; i<=n; ++i) y[++num]=i;
        for (int i=1; i<=n; ++i) if (sa[i]>k) y[++num]=sa[i]-k;
        for (int i=1; i<=m; ++i) c[i]=0;
        for (int i=1; i<=n; ++i) ++c[x[i]];
        for (int i=2; i<=m; ++i) c[i]+=c[i-1];
        for (int i=n; i>=1; --i) sa[c[x[y[i]]]--]=y[i],y[i]=0;
        swap(x,y);
        x[sa[1]]=1;
        num=1;
        for (int i=2; i<=n; ++i)
            x[sa[i]]=(y[sa[i]]==y[sa[i-1]] && y[sa[i]+k]==y[sa[i-1]+k]) ? num : ++num;
        if (num==n) break;
        m=num;
    }
}

/// height数组 (height[i] 表示sa[i-1]和sa[i]的最长公共公共前缀)
void get_height() {
    int k=0;
    for (int i=1; i<=n; ++i) rk[sa[i]]=i;
    for (int i=1; i<=n; ++i) {
        if (rk[i]==1) continue;
        if (k) --k;
        int j=sa[rk[i]-1];
        while (j+k<=n && i+k<=n && s[i+k]==s[j+k]) ++k;
        height[rk[i]]=k;
    }
}

/// st表  RMQ(height数组)
int dp[N][20];
void bd_st() {
    for (int i = 1; i <= n; i++) dp[i][0] = height[i];
    for (int j = 1; (1<<j) <= n; j++)
        for (int i = 1; i+(1<<j)-1 <= n; i++)
            dp[i][j] = min(dp[i][j-1],dp[i+(1<<(j-1))][j-1]);
}

/// height[rank[l]+1] .... height[rank[r]] 的最小值(求lcp时使用)
int rmq (int l, int r) {
    int k = 0;
    while ((1<<(k+1)) <= r-l+1) k++;
    return min(dp[l][k],dp[r-(1<<k)+1][k]);
}

/// 后缀x与后缀y的最长公共前缀
int qu_lcp(int x, int y) {
    if(x == y) return n - x + 1;
    x = rk[x],y = rk[y];
    if(x > y) swap(x, y);
    return rmq(x+1,y);
}

/// 注意二分的细节 很容易出错
int L,R;
void get_inv(int x,int len) {
    /// 求满足lcp(i,rk[x])的起始端点最小的后缀
    int l=1,r=rk[x],mid;
    while(l<r) {
        mid = (l+r)/2;
        if(qu_lcp(x,sa[mid])>=len) r=mid;
        else l=mid+1;
    }
    L = l;

    /// 求满足lcp(i,rk[x])的起始端点最大的后缀
    l=rk[x],r=n;
    while(l<r) {
        mid = (l+r+1)/2;
        if(qu_lcp(x,sa[mid])>=len) l=mid;
        else r=mid-1;
    }
    R = l;
}

/// -- 主席树部分 --
/// sa数组的值域为[1,n],可以不用离散化
int a[N],b[N];
int rt[N*20],ls[N*20],rs[N*20],sum[N*20],cnt=0;
void up(int pre,int& o,int l,int r,int pos) {
	o=++cnt;
	ls[o]=ls[pre];
	rs[o]=rs[pre];
	sum[o]=sum[pre]+1;
	if(l==r) return ;
	int m=(l+r)/2;
	if(pos<=m) up(ls[pre],ls[o],l,m,pos);
	else up(rs[pre],rs[o],m+1,r,pos);
}

int qu(int pre,int o,int l,int r,int k) {
	if(l==r) return l;///b[l];
	int m=(l+r)/2;
	if(sum[ls[o]]-sum[ls[pre]]>=k) return qu(ls[pre],ls[o],l,m,k);
	else return qu(rs[pre],rs[o],m+1,r,k-(sum[ls[o]]-sum[ls[pre]]));
}

int main() {
    scanf("%d",&t);
    while(t--) {
        scanf("%d%d%s",&n,&q,s+1);
        m=130;///字符串最大ASCII码值
        get_SA();
        get_height();
        bd_st();
        /*
        for(int i=1;i<=n;i++) a[i]=b[i]=sa[i];
        sort(b+1,b+n+1);
        int sz=unique(b+1,b+1+n)-(b+1);
		for(int i=1;i<=n;i++)
            a[i]=lower_bound(b+1,b+1+sz,a[i])-b;
        */
        cnt = 0;
        for(int i=1;i<=n;i++)
            up(rt[i-1],rt[i],1,n,sa[i]);
        while(q--) {
            int l,r,k;
            scanf("%d%d%d",&l,&r,&k);
            get_inv(l,r-l+1);
            if(R-L+1<k) printf("-1\n");
            else printf("%d\n",qu(rt[L-1],rt[R],1,n,k));
        }
    }
    return 0;
}

2、Comet OJ - 2019国庆欢乐赛 字符串

题意:https://www.cometoj.com/contest/68/problem/G?problem_id=3940

题解:

代码:

#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int N = 1e6+5;

char s[N],t[N];
int y[N],x[N],c[N],sa[N];
int n,m=180;///n为字符串长度,m为最大字符ASCII码

void get_SA() {
    memset(c,0,sizeof(c));
    for (int i=1; i<=n; ++i) ++c[x[i]=s[i]];
    for (int i=2; i<=m; ++i) c[i]+=c[i-1];
    for (int i=n; i>=1; --i) sa[c[x[i]]--]=i;
    for (int k=1; k<=n; k<<=1) {
        int num=0;
        for (int i=n-k+1; i<=n; ++i) y[++num]=i;
        for (int i=1; i<=n; ++i) if (sa[i]>k) y[++num]=sa[i]-k;
        for (int i=1; i<=m; ++i) c[i]=0;
        for (int i=1; i<=n; ++i) ++c[x[i]];
        for (int i=2; i<=m; ++i) c[i]+=c[i-1];
        for (int i=n; i>=1; --i) sa[c[x[y[i]]]--]=y[i],y[i]=0;
        swap(x,y);
        x[sa[1]]=1;
        num=1;
        for (int i=2; i<=n; ++i)
            x[sa[i]]=(y[sa[i]]==y[sa[i-1]] && y[sa[i]+k]==y[sa[i-1]+k]) ? num : ++num;
        if (num==n) break;
        m=num;
    }
}

int q,bl[N],sz[N],ans[N];
int main(){
    scanf("%d%s",&q,s+1);
    int len = strlen(s+1);
    int sl = len;
    s[++len] = 'z'+1;
    for(int i=1;i<=q;i++) {
        scanf("%s",t);
        int l = strlen(t);
        bl[len+1] = i;
        sz[i] = l;
        for(int j=0;j<l;j++)
            s[++len] = t[j];
        s[++len] = 'z'+2;
    }
    s[++len] = 'z'+2;
    n = len;
    get_SA();
    int mi = 1e9;
    for(int i=n;i>=1;i--) {
        if(sa[i]<=sl) mi = min(mi,sa[i]);
        else if(bl[sa[i]] && mi+sz[bl[sa[i]]]-1<=sl)
            ans[bl[sa[i]]] = mi;
    }
    for(int i=1;i<=q;i++) {
        if(ans[i]) printf("%d\n",ans[i]-1);
        else puts("-1");
    }
	return 0;
}

3、HDU 5008 Boring String Problem

题意:找到字符串中第k小(去重后)的子串,并且输出下标最小的那一个。

分析:看到子串问题(子串排序)就想到是后缀数组问题,这题纯粹是后缀数组性质的综合运用。因为子串是后缀的前缀,后缀数组对后缀排序的同时,也对子串进行了排序。对于每一个sa[i],会产生不同的n - sa[i] - height[i]个子串,这些子串也是排好序的。可以二分求出这些子串中的第k小的子串所在的后缀。但是还需要找到一个出现最早的,也就是满足条件的最小的sa[x]。有一个显然的结论,二分找到的后缀一定是这个k小子串出现的最后一个位置,不然就不是sa[i]贡献的新子串了。(可以用反证法证明,举例也很容易想明白。)所以我们二分找到右端点,如何在sa数组满足条件区间[l,r]内RMQ求最小就是这个子串最开始出现的位置。

代码:

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define inf 0x3f3f3f3f
const int N = 1e6+5;
char s[N];

int y[N],x[N],c[N],sa[N],rk[N],height[N];
int n,m;///n为字符串长度,m为最大字符ASCII码
/// -- 后缀树组模板部分 get_SA + get_height + bd_st() + rmq + qu_lcp 五个函数 --
/// 字符串下标从1开始,因为主席树需要前缀和做差
void get_SA() {
    memset(c,0,sizeof(c));
    for (int i=1; i<=n; ++i) ++c[x[i]=s[i]];
    for (int i=2; i<=m; ++i) c[i]+=c[i-1];
    for (int i=n; i>=1; --i) sa[c[x[i]]--]=i;
    for (int k=1; k<=n; k<<=1) {
        int num=0;
        for (int i=n-k+1; i<=n; ++i) y[++num]=i;
        for (int i=1; i<=n; ++i) if (sa[i]>k) y[++num]=sa[i]-k;
        for (int i=1; i<=m; ++i) c[i]=0;
        for (int i=1; i<=n; ++i) ++c[x[i]];
        for (int i=2; i<=m; ++i) c[i]+=c[i-1];
        for (int i=n; i>=1; --i) sa[c[x[y[i]]]--]=y[i],y[i]=0;
        swap(x,y);
        x[sa[1]]=1;
        num=1;
        for (int i=2; i<=n; ++i)
            x[sa[i]]=(y[sa[i]]==y[sa[i-1]] && y[sa[i]+k]==y[sa[i-1]+k]) ? num : ++num;
        if (num==n) break;
        m=num;
    }
}

/// height数组 (height[i] 表示sa[i-1]和sa[i]的最长公共公共前缀)
void get_height() {
    int k=0;
    for (int i=1; i<=n; ++i) rk[sa[i]]=i;
    for (int i=1; i<=n; ++i) {
        if (rk[i]==1) continue;
        if (k) --k;
        int j=sa[rk[i]-1];
        while (j+k<=n && i+k<=n && s[i+k]==s[j+k]) ++k;
        height[rk[i]]=k;
    }
}

/// st表  RMQ(height数组)
int dp[N][20],dp2[N][20];
void bd_st() {
    for (int i = 1; i <= n; i++) dp[i][0] = height[i];
    for (int j = 1; (1<<j) <= n; j++)
        for (int i = 1; i+(1<<j)-1 <= n; i++)
            dp[i][j] = min(dp[i][j-1],dp[i+(1<<(j-1))][j-1]);
    for (int i = 1; i <= n; i++) dp2[i][0] = sa[i];
    for (int j = 1; (1<<j) <= n; j++)
        for (int i = 1; i+(1<<j)-1 <= n; i++)
            dp2[i][j] = min(dp2[i][j-1],dp2[i+(1<<(j-1))][j-1]);
}

/// height[rank[l]+1] .... height[rank[r]] 的最小值(求lcp时使用)
int rmq (int l, int r) {
    int k = 0;
    while ((1<<(k+1)) <= r-l+1) k++;
    return min(dp[l][k],dp[r-(1<<k)+1][k]);
}

int rmq2 (int l, int r) {
    int k = 0;
    while ((1<<(k+1)) <= r-l+1) k++;
    return min(dp2[l][k],dp2[r-(1<<k)+1][k]);
}

/// 后缀x与后缀y的最长公共前缀
int qu_lcp(int x, int y) {
    if(x == y) return n - x + 1;
    x = rk[x],y = rk[y];
    if(x > y) swap(x, y);
    return rmq(x+1,y);
}

ll sum[N];
int main(){
    int q;
    while(scanf("%s",s+1)!=EOF) {
        n = strlen(s+1);
        m = 180;
        get_SA();
        get_height();
        bd_st();
        for(int i=1;i<=n;i++)
            sum[i] = sum[i-1] + n - sa[i] + 1 - height[i];
        scanf("%d",&q);
        ll l = 0, r = 0, v, k;
        for(int i=1;i<=q;i++) {
            scanf("%lld",&v);
            k = (l^r^v) + 1;
            if(k > sum[n]) {
                l = r = 0;
                puts("0 0");
                continue;
            }
            int L = 1,R = n, res = 0;
            while(L <= R) {
                int mid = (L + R) / 2;
                if(sum[mid] >= k) {
                    res = mid;
                    R = mid - 1;
                }else L = mid + 1;
            }
            int len = height[res] + k - sum[res - 1];
            L = res , R = n;
            int ans = res;
            while(L <= R) {
                int mid = (L + R) / 2;
                if(qu_lcp(sa[res],sa[mid]) >= len) {
                    ans = mid;
                    L = mid + 1;
                }else R = mid - 1;
            }
            l = rmq2(res,ans), r = l + len - 1;
            printf("%lld %lld\n",l,r);
        }
    }
	return 0;
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值