[数据结构]后缀数组

后缀数组是一个处理字符串的有力工具,对于常见的字符串匹配、模式匹配,求子串前缀等问题,后缀数组都有很好的效果。

但是好难懂啊。。搜了国家集训队的论文,看了挑战书上的介绍,看了好长时间才稍微有一点感觉。。就20多行的代码看了N久才理解…大概是我太弱了。

根据论文先来总结一下后缀数组的主要内容吧。

1、基本定义
子串:字符串S的子串r[i…j],i≤j,表示r串中从i到j这一段形成的字符串。
后缀:后缀是指从某个位置i开始到整个串末尾结束的一个特殊子串。
大小比较:字符串的比较,是指通常所说的字典序比较,比较规则是这样的:

对于两个字符串u、v,令i从1开始顺次比较u[i]和v[i]
如果u[i] = v[i] 则令i+1,继续比较
否则 若u[i] < v[i] 则认为u<v; u[i] > v[i],则认为 u > v。比较结束。
如果i>min(len(u),len(v))仍然比较不出结果,那就比较uv的长度,长度大的那个字符串就是更大的串。

后缀数组:后缀数组指的是将某个字符串的所有后缀按字典序排序后得到的数组。不过数组中并不需要直接保存所有的后缀字符串,只要记录对应的起始位置就好了。下文中,我们用S[i…]来表示字符串S从位置i开始的后缀。
名次数组:名次数组Rank[i]保存的是S[i]在所有后缀中从小到大排列的名字。

简单地说,后缀数组是“排第几的是谁?”名次数组是“你排第几?”。

举例:
[数据结构]后缀数组_第1张图片

大家可以根据这个字符串,自己计算一下Rank。

后缀数组的计算方法——倍增算法
假设我们要计算长度为n的字符串S的后缀数组,最朴素的做法就是把所有后缀进行排序,将n个长度为O(n)的字符串进行排序的复杂度为O(n²logn)。而如果灵活运用所有的字符串都是S的后缀这一性质,就可以得到更高效的算法。

该算法的基本思想是倍增。
首先计算每个位置开始长度为2的子串的顺序,再利用这个结果计算长度为4的子串顺序,接下来计算长度为8的子串的顺序,不断倍增,直到长度大于等于n,就得到了所有的后缀数组。并且这些子串一定已经比较出了大小,即Rank[]中已经没有相同的值,那么此时的Rank值就是最后的结果。每一次排序都利用上次长度为2^(k-1)的字符串的Rank值,那么长度为2^k的字符串就可以用两个长度为2^(k-1)的字符串的排名作为关键字表示,然后进行排序,就得出了长度为2^k的字符串的Rank值。

[数据结构]后缀数组_第2张图片

using namespace std;
const int MAXN = 100000;
int n,k;
int Rank[MAXN+1];
int tmp[MAXN+1];

//比较(rank[i],rank[i+k])和(rank[j],rank[j+k])
bool compare_sa(int i ,int j)
{
    if (Rank[i]!= Rank[j]) return Rank[i] < Rank[j];
    else
    {
        int ri = i+k<=n? Rank[i+k] : -1;
        int rj = j+k<=n? Rank[j+k] : -1;
        return ri < rj;
    }
}

//计算字符串S的后缀数组
void construct_sa(string S,int *sa)
{
    n = S.length();
    //rank直接获取字符的编码
    for(int i = 0;i<=n;i++)
    {
        sa[i] = i;
        Rank[i] = i < n ? S[i] : -1;
    }
    //利用对长度为k的排序结果对长度为2k的排序
    for(k = 1; k <= n; k *= 2)
    {
        sort(sa,sa+n+1,compare_sa);
        //先在temp中临时存储新计算的rank,再转存回rank中
        tmp[sa[0]] = 0;
        for(int i = 1 ; i <= n; i++)
            tmp[sa[i]] = tmp[sa[i-1]] + (compare_sa(sa[i-1],sa[i])? 1: 0);
        for(int i = 0 ; i <= n; i++)
            Rank[i] = tmp[i];
    }
}

你可能感兴趣的:([数据结构]后缀数组)