【算法笔记】后缀数组

后缀数组

  • 后缀数组
    • 0.参考资料
    • 1.相关定义
    • 2.倍增算法
    • 3.基数排序(桶排序)
    • 4.高度(height)数组
    • 5.后缀数组例题
    • 6.更新日志

前排提醒:坑未填完。【如果你看到这句话,请提醒博主填坑】


0.参考资料

国家集训队2009论文——罗穗骞
Hihocoder关于后缀数组的讲解
比较详细的一篇博客
【以及刘汝佳的小蓝书 和 我们学校内部的课件】


1.相关定义

【这里我们总假设数组/字符串的起始下标为 0,终止下标为 len-1】
定义 suffix(i) 为从 i 开始到串尾的后缀,称为“后缀i”
将字符串的所有后缀按字典序排序,可以得到这个字符串的后缀数组 sa(suffix array)。其中sa[i] 表示第 i 小的后缀为 suffix(sa[i])。
名次数组 rank 是 sa 的逆运算,即rank[sa[i]] = i,表示 suffix(i) 在所有后缀中的排名。因为长度不同的字符串不可能有相同的字典序,所以任意后缀的名次不并列。
【通俗地讲:sa[i] 表示排第 i 位的是谁,rank[i] 表示 i 排第几】【请一定要记住定义!!!】
假定某个字符串 S = “aabaaaab”。在它的末尾加入一个字符串中没有出现的字符比如”#”(等会儿我们解释为什么要加入尾字符)(原本应该是“$”来着……但是markdown会识别出来奇怪的东西)下面是它的sa和rank。
(UPD:原来还有转义字符这玩意儿……是博主low了)
【算法笔记】后缀数组_第1张图片
(假设 # 的优先级比字母小……哦不好像字典序的确 # 的优先级比字母小所以用不着假设233)

2.倍增算法

看到这里,我们大概可以瞎BB一个算法出来了:直接用sort进行排序。一共 n 个后缀,因此总复杂度 O(nlog2n) O ( n l o g 2 n ) 。因此,本算法被在合理时间复杂度内完美解决……?

注意到字符串之间的比较与字符串长成正比,所以算法复杂度将会是 O(n2log2n) O ( n 2 l o g 2 n ) 。平凡的 n 个字符串进行排序的复杂度是 O(n2log2n) O ( n 2 l o g 2 n ) (UPD:其实用字符串哈希可以得到一个O(nlog^2n)字符串排序算法来着,但是毕竟哈希的正确性并不稳定……),但是因为后缀之间是有联系的,所以我们可以设计出一个更优秀的算法:

该算法基于这样一个事实:两个字符串的比较,如果前半部分相同则比较结果决定于后半部分的比较结果,否则比较结果决定于前半部分的比较结果。
对于字符串 S S ,定义k-前缀为:
Sk=S[0...k1]k<=lenS S k = S [ 0... k − 1 ] ( k <= l e n S )
Sk=Sk>lenS S k = S ( k > l e n S )
然后,我们令 sak[i] s a k [ i ] k-前缀意义下的后缀数组, rankk[i] r a n k k [ i ] k-前缀意义下的名次数组。
一、容易求出 rank1 r a n k 1 sa1 s a 1 的值,只需要sort排序一次即可。
二、如果我们求出了 sak s a k ,我们可以很快的求出 rankk r a n k k
rankk[i]=rankk[i1]suffix(sa[i])k=suffix(sa[i1])k r a n k k [ i ] = r a n k k [ i − 1 ] ( s u f f i x ( s a [ i ] ) k = s u f f i x ( s a [ i − 1 ] ) k )
rankk[i]=rankk[i1]+1suffix(sa[i])ksuffix(sa[i1])k r a n k k [ i ] = r a n k k [ i − 1 ] + 1 ( s u f f i x ( s a [ i ] ) k ≠ s u f f i x ( s a [ i − 1 ] ) k )
三、然后,假如说我们求出了 rankk r a n k k ,则定义二元组 rankk[i],rankk[i+k] ( r a n k k [ i ] , r a n k k [ i + k ] ) 。以“ rankk[i]rankk[i+k]rankk[i] r a n k k [ i ] 相 等 , 则 比 较 r a n k k [ i + k ] , 否 则 比 较 r a n k k [ i ] ”的排序规则进行sort排序,即可求出 sa2k s a 2 k

也就是说算法流程是这样的:

sa1,rank1>sa2rank2>sa4rank4>...sa2mrank2m ( s a 1 , r a n k 1 ) − > ( s a 2 , r a n k 2 ) − > ( s a 4 , r a n k 4 ) − > . . . ( s a 2 m , r a n k 2 m )

什么时候停止呢?当不存在并列的rank的时候,所有后缀的大小关系就会被定义出来,这时候就可以停止算法了。
【算法笔记】后缀数组_第2张图片

(图片来源百度百科)
实际上第三次排序完就可以停止算法了。

每一次排序需要 nlog2n n l o g 2 n 次比较,每一次比较需要 O(1) O ( 1 ) 的时间复杂度,因此总时间复杂度为 O(nlog22n) O ( n l o g 2 2 n )
【快排我就不给参考代码了大家直接理解桶排序的代码好嘛】【怠惰的博主】

3.基数排序(桶排序)

O(nlog22n) O ( n l o g 2 2 n ) 的时间复杂度尽管很好,但还是不够优秀。

咱们先从后缀数组前走开,探究这样一个问题:
假设有 n 个数进行排序,则排序时间复杂度下限为 O(nlog2n) O ( n l o g 2 n )
但假设保证这n个数都是两位数,我们可以做到 O(n) O ( n ) 的算法。怎么做?看一下下面的例子。
【算法笔记】后缀数组_第3张图片
虽然实际运用中数据不可能总是两位数,但是假如我们把两位数看作一个二元组,十位看作第一关键字,个位看作第二关键字,并保证二元组的元素在能够存储的范围内,我们就可以实现对二元组的排序了。这种排序被形象地称为“桶排序”(基数排序)

我们先对字符串的元素进行离散化,元素的值域就变成了[0…n-1]。且离散化后我们发现 rank1[i]=S[i] r a n k 1 [ i ] = S [ i ] ,一举双得。然后每一次迭代中,不难发现 rank r a n k sa s a 都不会超过 n1 n − 1 (想想它们所代表的含义)。

于是得出结论:我们可以用基数排序代替每一轮循环中的sort。
一共进行 log2n l o g 2 n 次排序,每次排序遍历n个元素,n个桶,总时间复杂度 O(nlog2n) O ( n l o g 2 n )

【后缀数组还存在 O(n) O ( n ) 的构造方法 dc3 d c 3 ,但代码相对复杂。因为对于大多数题来说 O(nlog2n) O ( n l o g 2 n ) 的复杂度已经足够了,所以此处不再讨论。有兴趣的读者可以去游览一下集训队大佬的论文orz(其实是我自己没学懂233)】

参考实现如下(作者根据自己的理解所写的代码,与网上大部分代码不大相同,但是作者表示他觉得效率差不多)(因为网上的代码作者也看不懂233):

int arr[MAXN + 5], rnk[MAXN + 5], nrnk[MAXN + 5];
int bin[MAXN + 5], nxt[MAXN + 5], sa[MAXN + 5];
void InsertBIN(int x, int k) {
    nxt[x] = bin[k];
    bin[k] = x;
}
void GetSAFromBIN(int n) {
    int cnt = 0;
    for(int i=0;iwhile( bin[i] != -1 ) {
            sa[cnt++] = bin[i];
            bin[i] = nxt[bin[i]];
        }
    }
}
void BuildSA(int n) {
    for(int i=0;ifor(int i=0;i1;
    for(int i=0;ifor(int k=1;rnk[sa[n-1]]!=n-1;k<<=1) {
        for(int i=n-1;i>=0;i--)
            if( sa[i] >= k )
                InsertBIN(sa[i]-k, rnk[sa[i]-k]);
        for(int i=n-k;ifor(int i=0;i0]] = 0;
        for(int i=1;iint del = (nrnk[sa[i]] != nrnk[sa[i-1]]) || (nrnk[sa[i]+k] != nrnk[sa[i-1]+k]);//注1
            rnk[sa[i]] = rnk[sa[i-1]] + del;
        }
    }
}

【注1:这里sa[i]+k会访问到非法内存吗?不会。假如说 nrnk[sa[i]]==nrnk[sa[i1]] n r n k [ s a [ i ] ] == n r n k [ s a [ i − 1 ] ] ,则两个后缀的前半段一定不包含尾字符“#”,所以不会访问出界。否则因为C++短路运算符的特性,后面的语句不会执行】
【注2:实际上尾字符不一定要是“#”,让字符串长度++,字符串末尾就会多出现‘\0’这个字符,充当了尾字符的作用】

4.高度(height)数组

光有后缀数组并没有用的,我们还要借助它解决这样一个问题:LCP(Longest Common Prefix,最长公共前缀)。LCP的应用范围就非常广泛了,详情可以见下面的例题。
怎么解决呢?我们令 LCP(i,j)=lcp(suffix(i),suffix(j)) L C P ( i , j ) = l c p ( s u f f i x ( i ) , s u f f i x ( j ) ) ,并令 height[i]=LCP(sa[i],sa[i1]) h e i g h t [ i ] = L C P ( s a [ i ] , s a [ i − 1 ] ) 。对于满足 rank[i]<rank[j] r a n k [ i ] < r a n k [ j ] ,有这样一个结论:

LCP(i,j) L C P ( i , j )
=min(height[rank[i]+1],height[rank[i]+2],...height[rank[j]]) = m i n ( h e i g h t [ r a n k [ i ] + 1 ] , h e i g h t [ r a n k [ i ] + 2 ] , . . . h e i g h t [ r a n k [ j ] ] )
=RMQ(height,rank[i]+1,rank[j]) = R M Q ( h e i g h t , r a n k [ i ] + 1 , r a n k [ j ] )

这个结论的证明参考了上面提到的那篇博客,大家可以去看看那一位dalao的详细证明思路。证明如下:
【算法笔记】后缀数组_第4张图片

后缀数组要发挥威力,还必须依靠一个很重要的结论:

height[rank[i]]>=height[rank[i1]]1 h e i g h t [ r a n k [ i ] ] >= h e i g h t [ r a n k [ i − 1 ] ] − 1

证明大家可以看看论文或者博客。我这里让大家【感性认知】一下。
【算法笔记】后缀数组_第5张图片
下面是一个例子,大家可以对照着验证上面两个结论。
【算法笔记】后缀数组_第6张图片
下面是博主求height数组的参考实现代码。

void CalHeight(char *arr, int n) {
    int k = 0;
    for(int i=0;iif( rnk[i] == 0 ) height[rnk[i]] = 0;
        else {
            if( k ) k--;
            int j = sa[rnk[i]-1];
            while( arr[i+k] == arr[j+k] )
                k++;
            height[rnk[i]] = k;
        }
    }
}

5.后缀数组例题

暂缺,待填坑。

6.更新日志

UPD in 2018/8/12:对讲解部分进行了完善修改。

UPD in 2018/8/12:我想了想,例题大家还是看看dalao的论文吧,里面的例题非常丰富来着。如果之后我有遇到什么后缀数组的题的话,就加在这个地方

你可能感兴趣的:(--后缀数组!--,@字符串算法看这里!@)