后缀数组入门

转载自OI-wiki

后缀数组相关定义

sa[i]:表示按照字典序排序后,第i名后缀开头下标

rk[i]:表示后缀i的排名

后缀i:以下标i为开头的后缀

暴力求法

一切都从暴力开始,哈哈哈哈

我们把n个后缀sort一遍,复杂度是O(n2logn)。

倍增优化

使用倍增的思想进行优化。

  1. 按照每个后缀的前1个字母排序。

  2. 按照每个后缀的前2个字母排序

  3. 按照每个后缀的前4个字母排序

    ...

  4. 直到每个后缀的排名不一样

最多会排序logn次

具体排序

sa2

解释:

首先第一次按照每个后缀的第一个字母排序,每个后缀的排名为rk[i]

第二次按照后缀的前两个字母排序,将第i个后缀的排名和第i+1个后缀的排名作为第i个后缀的第一二关键字,如果i+1>n,第二关键字为0,进行排序。

这时rk[i]表示的是按照前两个字母排序后,后缀i的排名。

那么我们如何按照后缀的前四个值排序呢?

把第i个排名和第i+2的排名作为第i个的一二关键字,然后进行排序。

...

如果这个过程中使用的是sort排序,那么复杂度是O(nlog2n),

基数排序优化

使用基数排序将一次排序的复杂度降为O(n)

基数排序

代码

#include<bits/stdc++.h>
#define pb push_back
using namespace std;
typedef long long ll;
const int N=1e6+10;
const int mod=1e9+7;
const int inf=0x3f3f3f3f;

int pos[N],sa[N],cnt[N],rk[N],oldrk[N];
char str[N];
bool cmp(int x,int y,int k)
{
    return oldrk[x]==oldrk[y]&&oldrk[x+k]==oldrk[y+k];
}
void solve()
{
    int m=200;
    int n=strlen(str+1);
    for(int i=1; i<=n; ++i)
        ++cnt[rk[i]=str[i]];
    for(int i=1; i<=m; ++i)
        cnt[i]+=cnt[i-1];
    for(int i=n; i; --i)
        sa[cnt[rk[i]]--]=i;
    for(int k=1; k<=n; k<<=1)
    {
        int num=0;
        /*
        pos[i]表示第二关键字排名为i的第一关键字的位置
        n-k+1~n都没有第二关键字,补0,所以它们的排名在前面
        */
        for(int i=n-k+1; i<=n; ++i)
            pos[++num]=i;
        /*
        遍历sa,如果sa[i]>k,说明排名为i的可以作为第二关键字
        */
        for(int i=1; i<=n; ++i)
        {
            if(sa[i]>k)
                pos[++num]=sa[i]-k;
        }
        memset(cnt,0,sizeof(cnt));
        for(int i=1; i<=n; ++i)
            ++cnt[rk[i]];
        for(int i=1; i<=m; ++i)
            cnt[i]+=cnt[i-1];
        /*
        第一关键字相同的时候按照第二关键字由大到小进行排序
        */
        for(int i=n; i; --i)
            sa[cnt[rk[pos[i]]]--]=pos[i];
        /*
        排序完成之后,现在更新每个后缀的新排名
        */
        num=0;
        memcpy(oldrk,rk,sizeof(rk));//复制当前排名到oldrk。
        /*
        判断当前排名和前一排名的第一二关键字是否一样,如果一样num不用++
        否则num++
        使用cmp减少不连续内存访问,在数据范围较大时效果比较明显。
        */
        for(int i=1; i<=n; ++i)
            rk[sa[i]]=cmp(sa[i],sa[i-1],k)?num:++num;
        if(num==n)//有n个排名,排序完成
            break;
        m=num;//排名的最大值为num
    }
    for(int i=1; i<=n; ++i)
        rk[sa[i]]=i;
}
int main()
{
    scanf("%s",str+1);
    solve();
    int len=strlen(str+1);
    for(int i=1;i<=len;i++)
        printf("%d ",sa[i]);
    printf("\n");
    return 0;
}

height数组

lcp(i,j):表示后缀i和后缀j的最长公共前缀长度

height[i]=lcp(sa[i],sa[i-1])//第i名和第i-1名的最长公共前缀长度

height[1]=0

求height数组引理:

height[rk[i]]>=height[rk[i-1]]-1

证明:

Snipaste_2020-05-11_10-27-34

引自OI-WiKi。

O(n)求height数组代码:

利用引理暴力:

for (i = 1, k = 0; i <= n; ++i) 
{
  if (k) --k;//>=height[rk[i-1]]-1,所以减1
  while (s[i + k] == s[sa[rk[i] - 1] + k]) ++k;
  ht[rk[i]] = k; 
}

height数组简单应用

\(lcp(sa[i],sa[j])=min(height[i+1]...height[j])\)
\(lcp(i,j)=min(height[rk[i]+1]...height[rk[j]])\)

小声哔哔

当多组输入的时候,最好让str[n+1]等于一个没有出现过的字符,虽然没有出过错。

详情可见

posted @ 2020-05-18 19:08  Valk3  阅读(110)  评论(0编辑  收藏  举报