后缀数组总结

后缀数组(suffix array)是解决字符串问题非常有力的工具,主要是为了得到三个数组,分别是sa[i],rank[i],height[i]。先对字符串的各后缀进行排序,sa[i]表示排序后第i名的后缀它开始的位置,rank[i]表示从i这个位置往后的后缀它的排名是多少,显然sa和rank数组互逆,如果已知sa[i] = t,则rank[t] = i,height[i]表示排完序后第i个后缀和第i-1个后缀的lcp(最长公共前缀)长度。

具体算法实现大致可分为两类,一种是基于倍增+基数排序的思想,复杂度为O(nlogn),另一种是用DC3算法,代码比较麻烦,复杂度为O(n)。一般情况下用nlogn的方法就可以,比较好写而且非极端情况下不会被卡掉(其实还有更好写的二分+hash,nlogn*logn的复杂度,cmp里面二分判断字典序大小)。

放一个倍增的模板:

#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e6+10;
int n, m;
char s[maxn];
int sa[maxn], height[maxn], x[maxn], y[maxn], rk[maxn], tong[maxn];
void get_sa()
{
    for(int i = 1; i <= m; i++) tong[i] = 0;
    for(int i = 1; i <= 2*n; i++) x[i] = y[i] = 0;
    for(int i = 1; i <= n; i++) tong[x[i] = s[i]] ++;//初始第一关键字的排名就设置为其ASCII码即可
    for(int i = 2; i <= m; i++) tong[i] += tong[i-1];//计算前缀和,注意只算到m即可
    for(int i = n; i; i--) sa[tong[x[i]]--] = i;//以第一关键字计算排名
    for(int k = 1; k <= n; k <<= 1) {//倍增计算sa
        int num = 0;//计算Y,排名从1开始,指针指向0
        //Y[i] = j -> 以第二关键字排名,排名为i的首字母下标为j(即第j个后缀)
        for(int i = n-k+1; i <= n; i++) y[++num] = i;//后k个没有第二关键字,所以排名最小先分配排名
        for(int i = 1; i <= n; i++) {
            if(sa[i] <= k) continue;//前k个第一关键字不能作为某个后缀的第二关键字
            y[++num] = sa[i] - k;//sa为以第一关键字计算的排名,从小到大枚举排名,对应的下标其实是第 sa[i] - k 个后缀的第二关键字
        }
        for(int i = 0; i <= m; i++) tong[i] = 0;//初始化桶,范围为1-m即可
        for(int i = 1; i <= n; i++) tong[x[i]]++;//计算新的第一关键字每个排名有几个
        for(int i = 2; i <= m; i++) tong[i] += tong[i-1];//计算前缀和,1-m即可
        for(int i = n; i; i--) sa[tong[x[y[i]]]--] = y[i], y[i] = 0;
        //y[i] 表示以第二关键字排序,排名为i的后缀的下标
        //x[y[i]] 表示上述后缀按照第一关键字的排名
        //tong[x[y[i]]] 表示小于等于上述排名的数量,也就是该后缀的排名
        //sa记录上述排名对应的下标为 y[i],从后往前枚举第二关键字的排名,使得第一关键字相同的后缀也可以依靠第二关键字区分
        for(int i = 0; i <= 2*num; i++)
    	{
    		int temp = x[i];
    		x[i] = y[i];
    		y[i] = temp;
		}//接下来要更新X数组,且要用到旧的X数组,Y数组接下里用不到
        //则将两者交换,目的是将旧的X存到Y中,后面所有的Y实际就是旧的X
        // 将当前的第一关键字和第二关键字当做下一轮的第一关键字,sa中存的就是按照双关键字排序的结果。
        //swap比较慢,换成1~2*num的循环会快很多!
        x[sa[1]] = 1, num = 1;  //sa[1]对于的后缀新X的排名也为1
        for(int i = 2; i <= n; i++) 
            //如果新排名为i的后缀和新排名为i-1的后缀的第一关键字排名相同(前一个 == )
            //并且它们的第二关键字排名也相同(后一个 == ),那么两个后缀的新X排名相同,否则不同
            x[sa[i]] = (y[sa[i]] == y[sa[i-1]] && y[sa[i] + k] == y[sa[i-1] + k]) == 1 ? num : ++ num;
        if(n == num) return;//如果已经完全区分出n个后缀了,则可以结束循环
        m = num;//更新离散化后的rank范围
    }
}
//height[i] 表示排名为i和排名为i-1的后缀的最长公共前缀
void get_height() 
{
    for(int i = 1; i <= n; i++) rk[sa[i]] = i;//rk[i] = j - > 第i个后缀的排名为j
    for(int i = 1, k = 0; i <= n; i++) {
        if(rk[i] == 1) continue;//排名为1的height不用计算
        //设h[i]表示height[rk[i]],即位于第i个的后缀与排名在它前一个的后缀的最长公共前缀
        if(k) k--;//由于h[i]>=h[i-1]-1,所以从k-1开始枚举
        int j = sa[rk[i]-1];//排名在i前一个的后缀的下标
        while(i + k <= n && j + k <= n && s[i+k] == s[j+k]) k++;//如果相等,则最长公共前缀+1
        height[rk[i]] = k;//更新height
    }
}
void solve() 
{
    scanf("%s", s+1);  //下标从1开始
    n = strlen(s+1);
    //m每次都需要初始化,因为过程中会被更改
    m = 'z';//字符集的最大值,字符集最小值为1!
    get_sa();
    get_height();
    for(int i = 1; i <= n; i++) printf("%d ", sa[i]); 
    puts("");
	for(int i = 1; i <= n; i++) printf("%d ", rk[i]);  
    puts("");
    for(int i = 1; i <= n; i++) printf("%d ", height[i]); 
}
signed main()
{
    int t = 5; 
    while(t--) 
        solve();
    return 0;
}
/*
Input:
abab
排序后:
ab		id:3	rk:1
abab	id:1	rk:2
b		id:4	rk:3
bab 	id:2	rk:4
*/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值