后缀排序

后缀数组

一般可用于某一个字符串的子串有关字典序的问题。

算法流程

先对该字符串的所有后缀进行排序

定义:sa[i] 表示将所有后缀排序后第 i 小的后缀的编号。rk[i] 表示后缀i 的排名

采用倍增+基数排序(O(nlogn)),(如果偷懒/怕错也可以用快排(O(nlog2n)))

假设我们已经知道了长度为 w的子串的排名 rkw[1..n](即,rkw[i] 表示 s[i..min(i+w1,n)]s[x..min(x+w1,n)]x[1,n] 中的排名),那么,以 rkw[i] 为第一关键字,rkw[i+w] 为第二关键字(若 i+w>n 则令 rkw[i+w] 为无穷小)进行排序,就可以求出 rk2w[1..n]

关于基数排序,是对每一个关键词按优先级从大到小进行一次捅排序,复杂度和值域向关,于是把O(nlogn)一次的快排优化到了O(n)

可以参考算法可视化进行理解

原始代码

scanf("%s", s + 1);
  n = strlen(s + 1);
  m = max(n, 300);
  //m表示值域大小
  for (i = 1; i <= n; ++i) ++cnt[rk[i] = s[i]];
  for (i = 1; i <= m; ++i) cnt[i] += cnt[i - 1];
  for (i = n; i >= 1; --i) sa[cnt[rk[i]]--] = i;
  for (w = 1; w < n; w <<= 1) {
    //按rk[i+w]第一次捅排,把排序结果存入id中
    memset(cnt, 0, sizeof(cnt));
    for (i = 1; i <= n; ++i) id[i] = sa[i];
    for (i = 1; i <= n; ++i) ++cnt[rk[id[i] + w]];
    for (i = 1; i <= m; ++i) cnt[i] += cnt[i - 1];
    for (i = n; i >= 1; --i) sa[cnt[rk[id[i] + w]]--] = id[i];
    //按rk[i]第二次捅排,把排序结果存入sa中
    memset(cnt, 0, sizeof(cnt));
    for (i = 1; i <= n; ++i) id[i] = sa[i];
    for (i = 1; i <= n; ++i) ++cnt[rk[id[i]]];
    for (i = 1; i <= m; ++i) cnt[i] += cnt[i - 1];
    for (i = n; i >= 1; --i) sa[cnt[rk[id[i]]]--] = id[i];
    /*
    以上两次桶排也可以转化为以下简介的快排
    bool paix(int x,int y){
		return rk[x] == rk[y] ? rk[x + w] < rk[y + w] : rk[x] < rk[y];
	}
    sort(sa + 1, sa + n + 1, paix)	
    */
    memcpy(oldrk, rk, sizeof(rk));
    //根据sa的值重新更新rk的值(因为把以前排名一致的更新)
    for (p = 0, i = 1; i <= n; ++i) {
      if (oldrk[sa[i]] == oldrk[sa[i - 1]] &&
          oldrk[sa[i] + w] == oldrk[sa[i - 1] + w]) {
        rk[sa[i]] = p;
      } else {
        rk[sa[i]] = ++p;
      }
    }
  }

优化:

不需要对rk[i+w]排序,因为你已经知道了sa[1n],只需要对i+w大于n的处理成无限小,剩下的第i名就是sa[i]p

值域范围可以更新,我们在最后一段其实求出了当前的值域范围p,用它更新m

如果值域已经是n了,即rk已经互不相同,就可以结束

还有一些奇奇怪怪的卡常技巧(好像是连续访问一段内存会快一些)

总之,最后的代码如下

  int i, m = 300, p, w;
  scanf("%s", s + 1);
  n = strlen(s + 1);
  for (i = 1; i <= n; ++i) ++cnt[rk[i] = s[i]];
  for (i = 1; i <= m; ++i) cnt[i] += cnt[i - 1];
  for (i = n; i >= 1; --i) sa[cnt[rk[i]]--] = i;
  for (w = 1;; w <<= 1, m = p) {  // 优化二
    //优化一
    for (p = 0, i = n; i > n - w; --i) id[++p] = i;
    for (i = 1; i <= n; ++i)
      if (sa[i] > w) id[++p] = sa[i] - w;
    memset(cnt, 0, sizeof(cnt));
    //优化四
    for (i = 1; i <= n; ++i) ++cnt[px[i] = rk[id[i]]];
    for (i = 1; i <= m; ++i) cnt[i] += cnt[i - 1];
    for (i = n; i >= 1; --i) sa[cnt[px[i]]--] = id[i];
    memcpy(oldrk, rk, sizeof(rk));
    for (p = 0, i = 1; i <= n; ++i)
      //优化四
      rk[sa[i]] = cmp(sa[i], sa[i - 1], w) ? p : ++p;
    //优化三
    if (p == n) {
      for (int i = 1; i <= n; ++i) sa[rk[i]] = i;
      break;
    }
  }

运用

算法讲解到这,好像发现了一个问题:后缀数组的用途是不是有点少啊...好像只能处理个后缀的字典序,除此之外就没得用了...

其实,后缀数组真正有用的一个重要数组才刚刚上场

hight数组

定义:height[i]=lcp(sa[i],sa[i1]),即第i名的后缀与它前一名的后缀的最长公共前缀。

想一想,这种定义有什么好处,或者说,这种定义能做到什么

我们之前费尽周折,好不容易才把所有的后缀拍了个序,见下图

我们以排名为6的这个后缀为例(假如我们已经求出了所有hight)我们想知道这个后缀与所有其它后缀的前缀有多少是相同的,怎么办呢?

首先它与排名为5的后缀有hight[6]=2个部分是相同的,与排名为4的后缀有1个部分是相同的,排名为4的有1个,排名为3的有0个,排名为2的也只有0个,发现,排名为i的前缀与j的前缀的公共部分就有minjki{hight[k]}(当j>i也类似),为什么呢?

可以想一想字典序的相关定义,s1s2的字典序小,说明s1[1i]s2[1i]是相同,而s1[i+1]s2[i+1]不同,那么如果height 一直大于某个数,前这么多位就一直没变过。如果小于了某个数k,那s[k]之后的就不可能会再一次与他重合了。

我们再想一想这n个后缀还有什么性质

在求出每个hight之后,我们其实是对n个后缀的相等信息进行了压缩,而n个后缀的每一个前缀,又能完整的表示所有子串,这对我们处理子串的相等问题也大有帮助!

这就是hight数组在后缀数组种的运用实质,下面我们考虑怎么求出hight数组(终于开始将求法了...)

假设现在我们想得到hight[rk[i]]hight[i]表示的是lcp(sa[i],sa[i1])),其实可以利用hight[rk[i1]],这里给出一个引理height[rk[i]]height[rk[i1]]1

是不是看不懂,没关系,我们不用拘泥与这个式子,参考图想象一下。(下图中蓝色的一条和红色的一条表示后缀i1和后缀sa[rk[i1]1]相等的部分)

可以发现,hight[rk[i]]其实已经有了一个“保底”,即黑色下划线和绿色下划线的部分一定是相等的,但你无法保证sa[rk[i]1]就是x+1

其实没有关系,我们可以结合字典序考虑,如果现在有字符串sisxsx<si),而且已经有sisxy个字符是相等的,那么一定不存在一个sj使sx<sj<sisjsi相等的前缀长度小于y

所以说,在你求hight[rk[i]]的时候,已经有了hight[rk[i1]]1的“保底”,后缀sa[rk[i]1]与后缀i的前缀相同长度可以更长,但一定不会小于保底长度。

hight的代码,就接在求出sa之后。

for (i = 1, k = 0; i <= n; ++i) {
  if (k) --k;
  while (s[i + k] == s[sa[rk[i] - 1] + k]) ++k;
  ht[rk[i]] = k;  // height太长了缩写为ht
}

这就是后缀数组的基本流程(还是比较好理解的吧...)

下面上一些经典的题目

咕咕咕。。。。。。。

本文作者:qwq_123

本文链接:https://www.cnblogs.com/qwq-123/p/15904946.html

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   qwq_123  阅读(272)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
💬
评论
📌
收藏
💗
关注
👍
推荐
🚀
回顶
收起