[算法]后缀数组

前言
这篇博客真难写,暂定待更

定义
后缀数组[SuffixArray]是一个一维数组,简称SA,它保存1到n的某个排列\(SA[1] ,SA[2],\dots,SA[n]\),并且保证\(Suffix(SA[i])<Suffix(SA[i+1])\),\(1 \leq i < n\) 。也就是将s的n个后缀按字典序从小到大进行排序之后把排好序的后缀的编号顺次放入SA中。

我们先从后缀数组的入门题洛咕P3809讲起
名词解释
后缀[suffix]:
? 类似前缀,在字符串处理中意为对于一个初始字符串,以该字符串中任意元素为起始元素,最后一个元素为末尾元素的字串,即为该字符串的后缀
基数排序[Radix Sort]:
? 基数排序是桶排序的扩展,复杂度\(\Theta(n m log(r))\),其中r为所采取的基数,而m为堆数
? 基数排序[radix sort]属于"分配式排序[distribution sort]",又称“桶子法”[bucket sort]或[bin sort],它透过键值的部份资讯,将要排序的元素分配至某些“桶”中,藉以达到排序的作用
? 以上内容节选自百度百科
好了接下来讲如何构造后缀数组
构造后缀数组
首先我们将后缀编号

上图中编号为1的字符串为初始串,那么按如图所示给它编号为1-n。
s[i]:字符串的第i位
sa[i]:就是后缀数组,排名为i的后缀的编号
fst[i]:从编号为i的后缀的排名,即记录第一关键字排名(备注:在很多博客里这个数组的名字叫rank)
sec[i]:基数排序的第二关键字,表示以第二关键字排序时排名为i的后缀的编号
buk:桶
程序原理:
首先我们得形象理解fst,sec这两个十足。简单来说若当前后缀长度为len,fst是针对前\(\frac{len}{2}\)个字符的,sec是针对后\(\frac{len}{2}\)个字符的。
构造初始状态,按第一个字符基数排序的到了一个序列,此时第一个字母的相对关系我们已经知道了,暴力做法按第二个字母继续排序,但事实上我们需要这样做吗?
实际上有一点非常重要:编号为i的后缀的第二个字母,实际是编号为i+1的后缀的第一个字母。还有这种操作?就是有这种操作。又发现编号为i的后缀的第3,4个字母恰好是编号为i+2的后缀的前两个字母。天哪
这个性质有什么意义呢?
这就意味如果我们不断继续下去,我们珂以使用上一层的sec更新本层的fst,而不需额外计算
于是之后我们将会用到\(i+4,i+8,i+16,\dots\)的后缀计算i的第一关键字,我们珂以将其简单理解为一种倍增
于是我们愉快的一直计算,知道所有后缀的排名不同,然后结束
由于最多倍增计算到n,所以复杂度最大为\(\Theta(n log_2(n))\)
这样讲珂能(确实)很难理解,我们直接进入程序流程。

程序流程:
初始化:
根据字符串构造桶,并顺便初始化fst数组为该字母的ASCII码
这里我们要给桶求一个前缀,求玩前缀有什么好处呢?这珂以使桶中的数直接对应排名,方便计算
倍增循环部分:
待更。

P3809代码实现:

#include <cstdio>
#include <iostream>
#include <string>
#include <cstring>
#include <algorithm>

using namespace std;

namespace SuffixArray{
    //uses cstring, algorithm, cstdio. Program by lukelin
    #define MAXN 1000010 //len of max length
    #define MAXC 122 //len of max char (0, 255]
    #define ri register int
    int sa[MAXN], rnk[MAXN], fst[MAXN], sec[MAXN], buk[MAXN];
    char s[MAXN]; int s_len; int rnk_cnt;
    inline void clearBuk(){for (ri i = 0; i <= rnk_cnt; ++i) buk[i] = 0;}
    inline void getPrefixBuk(){for (ri i = 1; i <= rnk_cnt; ++i) buk[i] += buk[i - 1];}
    inline void printSA(){for (int i = 1; i <= s_len; ++i) printf("%d%c", sa[i], ((i == s_len)) ? '\n' : ' ');}
    void getSuffixArray(){
        s_len = strlen(s); rnk_cnt = MAXC;
        clearBuk();
        for (int i = 1; i <= s_len; ++i) ++buk[fst[i] = s[i - 1]];
        getPrefixBuk();
        for (int i = s_len; i > 0; --i) sa[buk[fst[i]]--] = i;
        for (int k = 1; k <= s_len; k <<= 1){
            int cnt = 0;
            for (int i = s_len - k + 1; i <= s_len; ++i) sec[++cnt] = i;
            for (int i = 1; i <= s_len; ++i) if (sa[i] > k) sec[++cnt] = sa[i] - k;
            clearBuk();
            for (int i = 1; i <= s_len; ++i) ++buk[fst[i]];
            getPrefixBuk();
            for (int i = s_len; i >= 1; --i) sa[buk[fst[sec[i]]]--] = sec[i], sec[i] = 0;
            std::swap(fst, sec);
            fst[sa[1]] = 1, rnk_cnt = 1;
            for (int i = 2; i <= s_len; ++i)
                fst[sa[i]] = (sec[sa[i]] == sec[sa[i - 1]] && sec[sa[i] + k] == sec[sa[i - 1] + k]) ? rnk_cnt : (++rnk_cnt);
            if (rnk_cnt == s_len) break;
        }
    }
    #undef MAXN
    #undef MAXC
    #undef ri
};

int main(){
    cin >> SuffixArray::s;
    SuffixArray::getSuffixArray();
    SuffixArray::printSA();
    return 0;
}

后缀数组思想
(未完待更)

原文地址:https://www.cnblogs.com/linzhengmin/p/10909205.html

时间: 2024-10-31 10:23:57

[算法]后缀数组的相关文章

后缀数组之倍增算法

首先说明 :后缀数组的构建在网上有多种方法:朴素的n*n*logn,还有倍增n*logn的,还有3*n的DC3算法,当然还有DC算法.这个算法学习自林厚丛老师的<高级数据结构>,代码较长,而且常数也比较大,但是是我这种笨人可以理解的.如有人想学短而快的可以学习<罗穗骞 后缀数组 ---处理字符串的有力工具>.顺便说一下,罗大神的算法书写的的确很短小也漂亮,可惜我看不懂. 说一下学习的心路历程吧!最开始想学后缀树,道理看明的了,可是一看代码实在是太长了(可能是我找的模版不对吧).后来

【算法学习】后缀数组

一个字符串的题,有姿势水平的OIers的脑中应该要浮现出许多算法-- 但是我没有姿势,也没有水平,除了KMP和trie树,什么也想不起来. 直到我学了它--后缀数组! 多亏这玩意儿,我现在什么都想不起来了. 后缀数组干嘛用的? 主要处理同一个字符串中的重复子串问题. 如何实现? 注意到每一个子串,都是一个后缀的某个前缀,这个后缀和前缀都是唯一确定的. 而后缀相同的前缀,和他们的字典序有密切联系.你有没有想过,字典中的相邻单词,他们的公共前缀总是很长. 一个字符串的任意后缀,都能用它的起始位置的下

算法学习:后缀数组 height的求取

[定义] [LCP]全名最长公共前缀,两个后缀之间的最长前缀,以下我们定义 lcp ( i , j ) 的意义是后缀 i 和 j 的最长前缀 [z函数] 函数z [ i ] 表示的是,第 i 个后缀和字符串的最长前缀  [解决问题] 这两个算法都是在解决这个问题 即求后缀和字符串和后缀之间的最长公共前缀 但是有所不同的是, 后缀数组最终求出的是,字典序第 i 个后缀和第 i + 1 个后缀的最长公共前缀 z函数最终求出的是,第 i 个后缀和字符串的最长公共前缀 然后通过这个最长公共前缀求一些其他

字符串匹配(三)----后缀数组算法

一.什么是后缀数组: 字符串后缀Suffix 指的是从字符串的某个位置开始到其末尾的字符串子串.后缀数组 Suffix Array(sa) 指的是将某个字符串的所有后缀按字典序排序之后得到的数组,不过数组中不直接保存所有的后缀子串,只要记录后缀的起始下标就好了. 比如下面在下面这张图中,sa[8] = 7,表示在字典序中排第9的是起始下标为7的后缀子串,这里还有一个比较重要的数组rank,rank[i] : sa[i]在所有后缀中的排名 ,比如rk[5]=0,表示后缀下标为5的子串在后缀数组中排

hdu 3518 Boring counting 后缀数组LCP

题目链接 题意:给定长度为n(n <= 1000)的只含小写字母的字符串,问字符串子串不重叠出现最少两次的不同子串个数; input: aaaa ababcabb aaaaaa # output 2 3 3 思路:套用后缀数组求解出sa数组和height数组,之后枚举后缀的公共前缀长度i,由于不能重叠,所以计数的是相邻height不满足LCP >= i的. 写写对后缀数组倍增算法的理解: 1.如果要sa数组对应的值也是1~n就需要在最后加上一个最小的且不出现的字符'#',里面y[]是利用sa数

HDU_6194 后缀数组+RMQ

好绝望的..想了五个多小时,最后还是没A...赛后看了下后缀数组瞬间就有了思路...不过因为太菜,想了将近两个小时才吧这个题干掉. 首先,应当认为,后缀数组的定义是,某字符串S的所有后缀按照字典序有小到大的顺序排列(使用下标表示后缀).因为具体过程没太看懂,但是参见刘汝佳蓝书<算法竞赛黑暗圣典>可以得到一个聪明的NLOGN的神器算法.不过这个不太重要. 之后还可以通过他在LCP问题中提到的RANK,height数组相关算法,处理出来height数组,之后其他的可以扔掉. <黑暗圣典>

后缀数组 DC3构造法 —— 详解

学习了后缀数组,顺便把DC3算法也看了一下,传说中可以O(n)复杂度求出文本串的height,先比较一下倍增算法和DC3算法好辣. DC3 倍增法 时间复杂度 O(n)(但是常数很大)   O(nlogn)(常数较小) 空间复杂度   O(n)    O(n) 编程复杂度    较高   较低 由于在时间复杂度上DC3的常数比较大,再加上编程复杂度比较高,所以在解决问题的时候并不是最优选择.但是学到了后缀数组还是补充一下的好点. DC3算法的实现: 1:先把文本串的后缀串分成两部分,第一部分是后

后缀数组(一堆干货)

其实就是将两篇论文里的东西整合在了一起,并且提供了一个比较好理解的板. 后缀数组 字符串:一个字符串S是将n个字符顺次排列形成的数组,n称为S的长度,表示为len(S).S的第i个字符表示为S[i]. 子串:字符串S的子串S[i…j],i<=j,表示从S串中从i到j这一段,也就是顺次排列S[i],S[i+1],……,S[j]形成的字符串. 后缀:后缀是指从某个位置i开始到整个字符串末尾结束的一个特殊子串.字符串S的从i开关的后缀表示为Suffix(S,i),也就是Suffix(S,i)=S[i…

POJ 3415 Common Substrings(后缀数组+单调栈)

[题目链接] http://poj.org/problem?id=3415 [题目大意] 求出两个字符串长度大于k的公共子串的数目. [题解] 首先,很容易想到O(n2)的算法,将A串和B串加拼接符相连, 做一遍后缀数组,把分别属于A和B的所有后缀匹配,LCP-k+1就是对答案的贡献, 但是在这个基础上该如何优化呢. 我们可以发现按照sa的顺序下来,每个后缀和前面的串的LCP就是区间LCP的最小值, 那么我们维护一个单调栈,将所有单调递减的LCP值合并, 保存数量和长度,对每个属于B串的后缀更新