后缀数组 学习笔记

后缀数组

定义

搞懂定义是学习后缀数组的关键。
子串:字符串 S 的子串 r[i..j] ,i ≤ j ,表示 r 串中从 i 到 j 这一段,就是顺次排列r[i],r[i+1],…,r[j]形成的字符串。
后缀:后缀是指从某个位置 i 开始到整个串末尾结束的一个特殊子串。字符串 r 的从 第 i 个字 符 开 始 的 后 缀 表 示 为 Suffix(i) ,也就是Suffix(i)=r[i..len(r)] 。
字符串的大小比较:关于字符串的大小比较,是指通常所说的 “ 字典顺序 ” 比较, 也就是对于两个字符串 u 、 v ,令 i 从 1 开始顺次比较 u[i] 和 v[i] ,如果u[i]=v[i] 则令 i 加 1 ,否则若 u[i] < <script type="math/tex" id="MathJax-Element-1"><</script>v[i] 则认为 u < <script type="math/tex" id="MathJax-Element-2"><</script>v , u[i]>v[i] 则认为 u>v(也就是 v < <script type="math/tex" id="MathJax-Element-3"><</script>u ),比较结束。如果 i>len(u) 或者 i>len(v) 仍比较不出结果,那么 若 len(u) < <script type="math/tex" id="MathJax-Element-4"><</script>len(v) 则 认 为 u < <script type="math/tex" id="MathJax-Element-5"><</script>v , 若 len(u)=len(v) 则 认 为 u=v , 若len(u)>len(v) 则 u>v 。
从字符串的大小比较的定义来看, S 的两个开头位置不同的后缀 u 和 v 进行比较的结果不可能是相等,因为 u=v 的必要条件 len(u)=len(v) 在这里不可能满足。
后缀数组: 后缀数组 sa 是一个一维数组,它保存 1..n 的某个排列 sa[1] ,sa[2] , …… , sa[n] ,并且保证 Suffix(sa[i]) < Suffix(sa[i+1]) , 1≤i < <script type="math/tex" id="MathJax-Element-6"><</script>n 。也就是将 S 的 n 个后缀从小到大进行排序之后把排好序的后缀的开头位置顺次放入 SA 中。
名次数组: 名次数组 Rank[i] 保存的是 Suffix(i) 在所有后缀中从小到大排列的 “ 名次 ” 。
height 数组: 定义 height[i]=suffix(sa[i-1]) 和 suffix(sa[i]) 的最长公共前缀,也就是排名相邻的两个后缀的最长公共前缀。那么对于 j 和 k ,不妨设rank[j] < <script type="math/tex" id="MathJax-Element-7"><</script>rank[k], 则有以下性质:suffix(j) 和 suffix(k) 的最长公共前缀为 height[rank[j]+1],height[rank[j]+2], height[rank[j]+3], … ,height[rank[k]] 中的最小值。

O(NlogN)求sa

倍增算法的主要思路:
用倍增的方法对每个字符开始的长度为 2k 的子字符串进行排序,求出排名,即 rank 值。 k 从 0 开始,每次加 1 ,当 2k 大于 n 以后,每个字符开始的长度为 2k 的子字符串便相当于所有的后缀。并且这些子字符串都一定已经比较出大小,即 rank 值中没有相同的值,那么此时的 rank 值就是最后的结果。每一次排序都利用上次长度为 2k1 的字符串的 rank 值,那么长度为 2k 的字符串就可以用两个长度为 2k1 的字符串的排名作为关键字表示,然后进行基数排序,便得出了长度为 2k 的字符串的 rank 值。

算法流程

  • 对长度为1的串进行排序。如果初始的字符集很小可以采用基数排序,字符集很大的话可以改成快速排序。
  • 进行倍增。每一次先对第二关键字进行排序,然后再对第一关键字进行排序。第二关键字的排序可以通过上一次算出的sa求出,第一关键字排序采用基数排序。
  • 对于求出的sa值求rank值。这里的rank可以不进行保留,而是根据字符串是否相等判断。

代码实现

void build_sa()
{
    m=n;
    for (int i=0;i<m;++i) c[i]=0;
    for (int i=0;i<n;++i) ++c[x[i]=s[i]];
    for (int i=1;i<m;++i) c[i]+=c[i-1];
    for (int i=n-1;i>=0;--i) sa[--c[x[i]]]=i;

    for (int k=1;k<=n;k<<=1)
    {
        p=0;
        for (int i=n-k;i<n;++i) y[p++]=i;
        for (int i=0;i<n;++i) if (sa[i]>=k) y[p++]=sa[i]-k;

        for (int i=0;i<m;++i) c[i]=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];

        swap(x,y);
        p=1;x[sa[0]]=0;
        for (int i=1;i<n;++i)
            x[sa[i]]=y[sa[i-1]]==y[sa[i]]&&((sa[i-1]+k<n?y[sa[i-1]+k]:-1)==(sa[i]+k<n?y[sa[i]+k]:-1))?p-1:p++;
        if (p>n) break;
        m=p;
    }
}

O(N)求rank/height

定义h[i]=height[rank[i]] ,也就是 suffix(i) 和在它前一名的后缀的最长公共前缀。
h 数组有以下性质:h[i] ≥ h[i-1]-1
证明:
设 suffix(k) 是排在 suffix(i-1) 前一名的后缀,则它们的最长公共前缀是h[i-1] 。那么 suffix(k+1) 将排在 suffix(i) 的前面(这里要求 h[i-1]>1 ,如果h[i-1] ≤ 1 ,原式显然成立 )并且 suffix(k+1) 和 suffix(i) 的最长公共前缀是h[i-1]-1 ,所以 suffix(i) 和在它前一名的后缀的最长公共前缀至少是 h[i-1]-1 。按照 h[1],h[2], …… ,h[n] 的顺序计算 , 并利用 h 数组的性质,时间复杂度可以降为 O(n) 。
上面的这段话看似难以理解,其实可以感性地感受一下。我们知道 Suffix(i-1) 去掉开头的一个字符就变成了 Suffix(i) ,同样Suffix(k) 去掉开头的一个字符就变成了 Suffix(k+1) ,如果 suffix(k) 是排在 suffix(i-1) 前一名的后缀,那么suffix(k+1) 肯定排在 suffix(i) 前面,并且它们都是由前面那两个后缀去掉一个字符得来的,所以它们之间去掉之后至少的公共前缀都不会变。
代码实现

void build_height()
{
    for (int i=0;i<n;++i) rank[sa[i]]=i;
    int k=0;height[0]=0;
    for (int i=0;i<n;++i)
    {
        if (!rank[i]) continue;
        if (k) --k;
        int j=sa[rank[i]-1];
        while (i+k<n&&j+k<n&&s[i+k]==s[j+k]) ++k;
        height[rank[i]]=k;
    }
}

问题和习题

1、最长公共前缀
Q:给定一个字符串,询问某两个后缀的最长公共前缀。
A:rmq求这两个后缀的区间最小值。

2、可重叠最长重复子串
Q:给定一个字符串,求最长重复子串,这两个子串可以重叠。
A:求height数组里的最大值。

3、不可重叠最长重复子串
Q:给定一个字符串,求最长重复子串,这两个子串不能重叠。
A:先二分答案,把题目变成判定性问题:判断是否存在两个长度为mid的子串是相同的,且不重叠。把排序后的后缀分成若干组,其中每组的后缀之间的 height 值都不小于 k 。有希望成为最长公共前缀不小于 k 的两个后缀一定在同一组。 然后对于每组后缀,只须判断每个后缀的 sa 值的最大值和最小值之差是否不小于 k 。如果有一组满足,则说明存在,否则不存在。
pku 1743

4、可重叠的 k 次最长重复子串
Q:给定一个字符串,求至少出现 k 次的最长重复子串,这 k 个子串可以重叠。
A:这题的做法和上一题差不多,也是先二分答案,然后将后缀分成若干组。 不同的是,这里要判断的是有没有一个组的后缀个数不小于 k 。如果有,那么存在 k 个相同的子串满足条件,否则不存在。
pku 3261

5、不相同的子串的个数
Q:给定一个字符串,求不相同的子串的个数。
A:每个子串一定是某个后缀的前缀,那么原问题等价于求所有后缀之间的不相同的前缀的个数。如果所有的后缀按照suffix(sa[1]), suffix(sa[2]),suffix(sa[3]), …… ,suffix(sa[n]) 的顺序计算,不难发现,对于每一次新加进来的后缀suffix(sa[k]), 它将产生 n-sa[k]+1 个新的前缀。但是其中有 height[k] 个是和前面的字符串的前缀是相同的。所以 suffix(sa[k]) 将 贡献出 n-sa[k]+1-height[k] 个不同的子串。累加后便是原问题的答案。
spoj 694
spoj 705

6、最长回文子串
Q:给出一个字符串,求最长回文子串。
A:枚举每一位,然后计算以这个字符为中心的最长回文子串。注意这里要分两种情况,一是回文子串的长度为奇数,二是长度为偶数。两种情况都可以转化为求一个后缀和一个反过来写的后缀的最长公共前缀。具体的做法是:将整个字符串反过来写在原字符串后面,中间用一个特殊的字符隔开。这样就把问题变为 了求这个新的字符串的某两个后缀的最长公共前缀。
ural 1297

7、连续重复子串
Q:给定一个字符串 L ,已知这个字符串是由某个字符串 S 重复 R 次而得到的,求 R 的最大值。
A:枚举字符串 S 的长度 k ,然后判断是否满足。判断的时候,先看字符串 L 的长度能否被 k 整除,再看 suffix(1) 和 suffix(k+1) 的最长公共前缀是否等于 n-k 。在询问最长公共前缀的时候, suffix(1) 是固定的,所以 RMQ 问题没有必要做所有的预处理,只需求出 height 数组中的每一个数到 height[rank[1]] 之间的最小值即可。
pku 2406

8、重复次数最多的连续重复子串
Q:给定一个字符串,求重复次数最多的连续重复子串。
A:先枚举长度 L ,然后求长度为 L 的子串最多能连续出现几次。首先连续出现 1 次是肯定可以的,所以这里只考虑至少 2 次的情况。假设在原字符串中连续出现 2 次,记这个子字符串为 S ,那么 S 肯定包括了字符 r[0], r[L], r[L*2], r[L*3], …… 中的某相邻的两个。所以只须看字符 r[L*i] 和 r[L*(i+1)] 往前和往后各能匹配到多远,记这个总长度为 K ,那么这里连续出现了 K/L+1 次。最后看最大值是多少。
spoj 687
pku 3693

9、最长公共子串
Q:给出两个字符串,求最长公共子串。
A:字符串的任何一个子串都是这个字符串的某个后缀的前缀。求 A 和 B 的最长公共子串等价于求 A 的后缀和 B 的后缀的最长公共前缀的最大值。由于要计算 A 的后缀和 B 的后缀的最长公共前缀,所以先将第二个字符串写在第一个字符串后面,中间用一个没有出现过的字符隔开,再求这个新的字符串的后缀数组。当 suffix(sa[i-1]) 和suffix(sa[i]) 不是同一个字符串中的两个后缀时,height[i] 的最大值就是答案。
pku 2774
ural 1517

10、长度不小于 k 的公共子串的个数
Q:给定两个字符串 A 和 B ,求长度不小于 k 的公共子串的个数(可以相同)。
A:首先把一个串接在另一个串的后面,中间放一个没出现过的字符。由于每一个子串都是某一个后缀的前缀,求出sa和height了之后,我们可以将height分组,组内都是height>=k的后缀。可以知道长度不小于k的公共子串是两个后缀的前缀,并且它们一定在同一组内。 那么对于每一组,从前往后扫,假设遇到了A的后缀,那么统计一下它前面与B的后缀能组成多少>=k的公共子串,然后再把A和B反一下,这样扫两遍,就求出了答案。 关键是怎么统计。暴力统计是 O(n2) 的肯定不行。我们知道两个后缀的最长公共前缀是它们的区间最小值,所以可以维护一个自底向上单调递增的栈。同时需要维护的是,栈中的总和已经栈中每一个元素的出现次数。需要注意的是,只有需要统计的一种后缀的height是有价值的,不需要统计的一种后缀的height需要入栈,但是没有价值,这里的价值也就是上文说道的“出现次数”。每一次弹栈,相当于是用较小的height替换了较大的height,但是总个数不能改变。
pku 3415

11、不小于 k 个字符串中的最长子串
Q:给定 n 个字符串,求出现在不小于 k 个字符串中的最长子串。
A:将 n 个字符串连起来,中间用不相同的且没有出现在字符串中的字符隔开,求sa和height。然后二分答案,将后缀分成若干组,判断每组后缀是否出现在不小于 k 个的原串中。
pku 3294

12、每个字符串至少出现两次且不重叠的最长子串
Q:给定 n 个字符串,求在每个字符串中至少出现两次且不重叠的最长子串。
A:做法和上题大同小异,也是先将 n 个字符串连起来,中间用不相同的且没有出现在字符串中的字符隔开,求后缀数组。然后二分答案,再将后缀分组。判断的时候,要看是否有一组后缀在每个原来的字符串中至少出现两次,并且在每个原来的字符串中,后缀的起始位置的最大值与最小值之差是否不小于当前答案(判断能否做到不重叠)。
spoj 220

13、出现或反转后出现在每个字符串中的最长子串
Q:给定 n 个字符串,求出现或反转后出现在每个字符串中的最长子串。
A:这题不同的地方在于要判断是否在反转后的字符串中出现。其实这并没有加大题目的难度。只需要先将每个字符串都反过来写一遍,中间用一个互不相同的且没有出现在字符串中的字符隔开,再将 n 个字符串全部连起来,中间也是用一个互不相同的且没有出现在字符串中的字符隔开,求后缀数组。然后二分答案,再将后缀分组。判断的时候,要看是否有一组后缀在每个原来的字符串或反转后的字符串中出现。
pku 1226

小结

把论文里的题做完了,这些题目都比较基础,难一点的题目还是要再做一点。
可以发现,后缀数组的题目关键字就一个:
子串
无非就两个关键性质:
①子串一定是某一个后缀的前缀
②最长公共前缀是height的区间最小值
而求解这一类基本问题也就几个方法:
①二分
②height分组
③st表
④单调栈

题目

(题目难度尽量升序)
bzoj 1031
bzoj 1717
bzoj 1692
bzoj 2251
bzoj 3230
bzoj 3238
bzoj 4516
bzoj 2119
bzoj 3277

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值