后缀排序

后缀数组

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

算法流程

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

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

采用倍增+基数排序(\(O(n\log n)\)),(如果偷懒/怕错也可以用快排(\(O(n\log^2 n )\)))

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

关于基数排序,是对每一个关键词按优先级从大到小进行一次捅排序,复杂度和值域向关,于是把\(O(n\log n)\)一次的快排优化到了\(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[1\sim n]\),只需要对\(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[i−1])\),即第$ i $名的后缀与它前一名的后缀的最长公共前缀。

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

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

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

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

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

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

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

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

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

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

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

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

所以说,在你求\(hight[rk[i]]\)的时候,已经有了\(hight[rk[i-1]]-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
}

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

下面上一些经典的题目

咕咕咕。。。。。。。

posted @ 2022-02-17 15:45  qwq_123  阅读(253)  评论(0编辑  收藏  举报