后缀数组

眉间皱痕 提交于 2020-02-17 02:12:07

看了好久,吐了,这个东西打脑阔得很。
首先得介绍一下基数排序

//#include<bits/stdc++.h>
#include<iostream>
#include<cstdio>
using namespace std;
//基数排序:一种稳定的,非比较的排序方式。分为LSD和MSD两种方式,LSD通过从最低为开始,MSD从高位开始排,
//将所有数的同一位分配到桶中,并通过合并桶的方式进行一个部分排序,持续这样的操作直到最高位
int getmax(int a[],int n) {
    int res = 1, p = 10;
    for(int i = 0;i < n; ++i) {
        while(a[i] >= p) {
            res++,p *= 10;
        }
    }
    return res;
}
int main()
{
    int a[1000],count[1000],tmp[1000];
    int radix = 1;
    const int N = 10;
    for(int i = 0;i < N; ++i) scanf("%d",&a[i]);
    int maxbit = getmax(a,N);
    //cout<<maxbit<<endl;
    for(int i = 1;i <= maxbit; ++i) {
        for(int j = 0;j < 10; ++j) count[j] = 0;
        for(int j = 0;j < N; ++j) {
            count[(a[j] / radix) % 10]++;
        }  //将每个数分配到桶中
        //合并桶
        for(int j = 1;j < 10; ++j) count[j] += count[j - 1];  //统计比当前数小的
        for(int j = N - 1;j >= 0; --j) {  //为什么要倒着枚举:因为要考虑前一位比较的结果不变,倒序枚举保证了先把前一位大的数的位置安排了,在本位相等的情况下,上一位大的一定排在后面,在这一次排序中排名也更应该靠后。
            int id = (a[j] / radix) % 10;   //应该分配的桶
            tmp[count[id] - 1] = a[j];//tmp[i]表示排名为i的数是tmp[i];
            count[id] --;
        }
        for(int j = 0;j < N; ++j) a[j] = tmp[j];
        radix *= 10;
    }
    for(int i = 0;i < N; ++i) printf("%d ",a[i]);
    cout<<endl;
    return 0;
}

模拟的过程:
排序:73, 22, 93, 43, 55, 14, 28, 65, 39, 81
在这里插入图片描述
后缀数组:
对于一个长度为n的字符串s,我们用suff[i]表示从s[i]开始的后缀;
一.首先是两个数组:
后缀数组sa:sa[i]表示s的所有后缀按字典序排序后排在第i位的后缀是suff[sa[i]];也就是sa[排名] = 位置
排名数组rank:rank[i]表示suff[i]在s的所有后缀中的排名是rank[i];也就是rank[位置] = 排名;
二、后缀数组的求取:有两种算法:倍增和dc3,倍增(O(nlogn))时间复杂度比不上dc3 O(n),但其他方面都是倍增更有优势,在此仅介绍倍增
倍增算法:首先我们在整个字符串后面添加一个未出现的最小值。然后求取从每个位置开始长度为1的后缀的sa数组求出来,这里的sa数组意义稍有不同:sa[i]表示排名为i的长度为1的后缀的起始位置是sa[i];此后我们就可以倍增长度,分别求出每个位置开始的长度为2,4,8…直到n或者大于n的后缀的排名,即最终的sa数组。直观上看就是下图;
在这里插入图片描述
上模板代码

//例子:aabaaaab0
void DA(int n,int m) {
    //字符串长度是n,求解sa的数组中最大值不超过m,就像对于十进制数来说,使用基数排序只需要0~9 10个桶,因为每一位不会超过9,这里就是把数换成了字母。
    n++;//末尾添加了一个最小值,rank[n-1]无效,sa[0]无效,从n-1开始的后缀和排名是0的后缀一定是以我们添加的字符开始,无效,但是添加这个字符会让我们求取更方便;
    int *x = x1,*y = y1;//x数组是一个虚假的rank数组,真正的rank数组每个后缀的排名都不一样,因为长度不一样,而在求解sa的过程中,长度是倍增上去的,因此中途可能出现完全相同的后缀,比如aabaaaab在长度为2的时候,s[0]和s[3]开始的后缀都是aa,所以他们名次一样,所以x是虚假的rank数组
    //求解长度为1的后缀排名,使用的是基数排序,如果m过大换成快排
    for(int i = 0;i < m; ++i) c[i] = 0; //把桶清零
    for(int i = 0;i < n; ++i) c[x[i] = a[i]]++;//把当前位的数放到对应的桶中去,x保存排名,对应例子来说x[0] = 97(a的ascll码),表示从0开始的长度为1的后缀的排名是97,x[2] = 98表示从2开始的长度为1的后缀的排名是98
    for(int i = 1;i < m; ++i) c[i] += c[i-1];//采用累加的方式计算比当前小的数有多少个,对应例子:x[97] = 7,x[98] = 9;
    for(int i = n-1;i >= 0; --i) sa[--c[x[i]]] = i;//倒序枚举每一个位置,比x[i]小的数有c[x[i]]个(包括自身),所以x[i]的排名就是c[x[i]]-1,而sa意义是sa[排名]=位置,此时的位置是i
    int p;
    for(int k = 1;k < n; k <<= 1) { //多次基数排序
        //倍增求取长度更长的后缀排名;从图中可以看出来,从长度k求取长度k*2时,需要知道两个长度为k的排名并将这两个排名组合为一个,我们称第一个排名为第一关键字,第二个为第二关键字;
        //如果要知道长度为k*2的排名,就要分别比较第一关键字和第二关键字,第二关键字对结果的影响较低,相当于两位数的个位;所以先排第二关键字,在排第一关键字
        //显然,从n-k到n-1这些位置开始的字符串是没有第二关键字的,我们认为其为0;
        //第二关键排序,y数组保存结果y[p]表示排名为p长度为k*2的后缀的第一关键字的位置是y[p]; 也是y[排名] = 位置
        p = 0; //排名计数器
        for(int i = n - k;i < n; ++i) y[p++] = i;//n-k~n-1第二关键字为0,自然排名排在前面;
        for(int i = 0;i < n; ++i)
            if(sa[i] >= k) y[p++] = sa[i] - k; //如果sa[i]>=k代表从sa[i]开始的长度为k的后缀可以和sa[i]-k这个位置开始的长度为k的字符串组成长度为2*k的串;并且以sa[i]-k开始的串的排名作为第一关键字,sa[i]开始的串的排名作为第二关键字
        //第一关键字排序,这是建立在第二关键字排好序的基础上的,所以我们排的时候不能改变第二关键字排好的结果
        for(int i = 0;i < m; ++i) c[i] = 0; //桶清0
        for(int i = 0;i < n; ++i) c[x[y[i]]]++; //把第一关键字入桶
        for(int i = 1;i < m; ++i) c[i] += c[i-1];
        for(int i = n-1;i >= 0; --i) sa[--c[x[y[i]]]] = y[i]; //计算新的sa的值,把针对长度为k的sa更新为长度为2*k的;
        swap(x,y); //采用指针,避免复制整个数组;
        //更新新的名次数组x,因为有可能有两个相等字符串,其排名一样,所以要进行比较,而最可能一样的就是排名相邻的两个字符串;
        p = 1;x[sa[0]] = 0;
        for(int i = 1;i < n; ++i) {
            x[sa[i]] = (cmp(y,sa[i],sa[i-1],k) ? p - 1 : p++); 
        }
        if(p >= n) break; //如果排名数组x中每个后缀的排名都不一样,即p==n,那么再排序也不会产生影响,可以结束,就像图中第四次排序是没有必要的; 
        m = p;  //求取桶的最大值;
    }
	n--;
    for(int i = 0;i <= n; ++i) rk[sa[i]] = i; 

至此,sa数组和rank数组就求取完毕了.来道板子题
洛谷3809
最长公共前缀LCP
仅有sa和rank数组还是没法做题,我们通过它们可以求解出任意两个后缀最长公共前缀长度;
定义:LCP(i,j)代表suff[sa[i]]和suff[sa[j]]的最长公共前缀的长度
性质:
1.LCP(i,k) = min(LCP(i,j),LCP(j,k)); (i<=j<=k);
2.LCP(i,k) = min(LCP(j,j-1)) (i<j<=k);
LCP的求解在基于后缀数组的思想下,要用到height数组:height[i] = LCP(i,i-1);即排名相邻的两个后缀的最长公共前缀的长度,暴力求解O(n^2),但是height数组有个性质:height[rank[i]] >= height[rank[i-1]] - 1;
解释:假设排名是rank[i-1]-1的后缀从下标k开始,现在讨论下标是k+1和i开始的后缀的关系;
如果下标从i-1和k开始的两个后缀的首字符不相等,那么下标是k+1和i开始的两个后缀关系不定,但是可以得到height[rank[i-1]] = 0,上面结论显然成立
如果下标从i-1和k开始的两个后缀的首字符相等,那么显然k+1开头的后缀一定排在i开头的后缀的前面,此时有height[rank[i]] = height[rank[i-1]] - 1;但是还有可能存在其他的后缀与i开头的后缀有更长的LCP;并且这些其他后缀和i开头的后缀在排名上相邻,所以就有height[rank[i]] >= height[rank[i-1]] - 1;
在height数组的求取上依据这个性质采用了递推的求法

int k = 0;
for(int i = 0;i < n; ++i) { //枚举位置
    if(k) --k;
    int j = sa[rk[i] - 1];  //选取最有可能有最大公共前缀的即排名相邻的
    while(a[i + k] == a[j + k]) ++k;
    height[rk[i]] = k;
}

后缀数组应用
1,求取两个字符串的最长公共子串:
把字符串连成一串,中间加个分隔的值,然后求height数组,找到最大的并且分别位于两个串就可以了;
//待更
参考:基数排序
添加链接描述

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!