后缀数组(SA) 学习笔记

· 算法

天梯赛校赛碰到的 本以为切个水题 没想到捅了老挝

甚至还有n,q=10w的区间众数 2333 就是数据比较水 感觉严一点分块都不一定能过

原题貌似是这个 但是他妈的校赛n改到了2w

一开始感觉是差分+lcs或kmp之类的 然后感觉不太对劲...

还是需要一些处理字符串的手段 于是学一下SA和SAM 已经拖了挺久了...

前面的定义和求解过程理解移步oi-wiki即可

那上面有图解,看的更清楚

定义

对于一长为nn的字符串 ss 即s[1−n]s[1-n] 令suf[i]=s[i−n]suf[i]=s[i-n] 称为后缀ii

将ss的nn个后缀排序

  • sa[i]sa[i]表示第ii小的后缀的编号 即suf[sa[i]]suf[sa[i]]是第ii小的后缀
  • rk[i]rk[i]表示后缀ii的排名

即sa[rk[i]]==rk[sa[i]]=isa[rk[i]]==rk[sa[i]]=i

求后缀数组

O(n2logn)O(n^2logn)求法很显然。直接暴力排序就行了

考虑两个连续长为1的子串 即s[i]s[i]和s[i+1]s[i+1]的排名rk0[i]rk_0[i]和rk0[i+1]rk_0[i+1]作为两个关键字

能够得到所有长为2的子串的sa1[i]sa_1[i] 通过sa1[i]sa_1[i]得到rk1[i]rk_1[i] 然后再求sa2[i]sa_2[i]

以此类推得到saw[i]sa_w[i]表示子串s[i→i+2w−1]s[i\to i+2^w-1]的后缀数组

整个过程就是用s[i→i+2j−1]s[i\to i+2^j-1]的子串排名即rkrk数组去求解 s[i→i+2j+1−1]s[i\to i+2^{j+1}-1]的子串的后缀数组sasa

能够倍增的原因就是因为连续的字符串排字典序 前面优先级大于后面 折半做就很显然

当2w≥n2^w\geq n时即为整个串的后缀数组

#include <bits/stdc++.h>
using namespace std;
int main() {
  string s;
  cin >> s;
  int n = s.size();
  s = '$' + s;
  vector<int> sa(n + 1), rk(n + 1 << 1), ork(n + 1 << 1);
  for (int i = 1; i <= n; ++i)
    sa[i] = i, rk[i] = s[i];
  for (int w = 1; w < n; w <<= 1) {
    sort(sa.begin() + 1, sa.end(), [&](int x, int y) {
      return rk[x] == rk[y] ? rk[x + w] < rk[y + w] : rk[x] < rk[y];
    });
    ork = rk;
    for (int p = 0, i = 1; i <= n; ++i)
      if (ork[sa[i]] == ork[sa[i - 1]] && ork[sa[i] + w] == ork[sa[i - 1] + w])
        rk[sa[i]] = p;
      else
        rk[sa[i]] = ++p;
  }
  for (int i = 1; i <= n; ++i)
    cout << sa[i] << " \n"[i == n];
  return 0;
}

倍增+sort 中间还有个更新操作 就是O(nlogn+n)=O(nlogn)O(nlogn+n)=O(nlogn)的复杂度

呃呃 然后按理说O(nlognlogn)O(nlog_nlog_n)是过不去的 但是C++17开了O2居然就直接过了... 过了...

submission

但是语言与时俱进的同时 丧心病狂的出题人肯定也会与时俱进 所以我们还是要学习标准算法的 233


复杂度降到 O(NlogN)O(NlogN)

计数排序和基数排序 OI-wiki随便看下就好了 没有什么难度

如何快速理解计数排序?

某种意义上可以当成桶排来理解

const int N = 100010;
const int W = 100010;

int n, w, a[N], cnt[W], b[N];

void counting_sort() {
    memset(cnt, 0, sizeof(cnt));
    for (int i = 1; i <= n; ++i) ++cnt[a[i]];
    for (int i = 1; i <= w; ++i) cnt[i] += cnt[i - 1];
    for (int i = n; i >= 1; --i) b[cnt[a[i]]--] = a[i];
}

如何快速理解基数排序?

多关键字排序,每个关键字可以选择一个合适的方法(一般是计数排序)排完之后保证稳定性,然后排下一个关键字

基于计数排序实现的基数排序可以理解成桶排的桶排

基于这个理念,我们依赖O(n)的辅助存储空间代替sort的比较

#include <bits/stdc++.h>
using namespace std;
int main() {
  string s;
  cin >> s;
  int n = s.size();
  s = '$' + s;
  int m = 127;
  vector<int> sa(n + 1), rk((n + 1) << 1), cnt(m + 1);
  for (int i = 1; i <= n; ++i)
    ++cnt[rk[i] = s[i]];
  for (int i = 1; i <= m; ++i)
    cnt[i] += cnt[i - 1];
  for (int i = n; i >= 1; --i)
    sa[cnt[rk[i]]--] = i;
  auto _rk = rk;
  for (int p = 0, i = 1; i <= n; ++i)
    if (_rk[sa[i]] == _rk[sa[i - 1]])
      rk[sa[i]] = p;
    else
      rk[sa[i]] = ++p;
  m = n;
  cnt.resize(m + 1);
  for (int w = 1; w < n; w <<= 1) {
    fill(cnt.begin(), cnt.end(), 0);
    auto _sa = sa;
    for (int i = 1; i <= n; ++i)
      ++cnt[rk[_sa[i] + w]];
    for (int i = 1; i <= m; ++i)
      cnt[i] += cnt[i - 1];
    for (int i = n; i >= 1; --i)
      sa[cnt[rk[_sa[i] + w]]--] = _sa[i];

    fill(cnt.begin(), cnt.end(), 0);
    _sa = sa;
    for (int i = 1; i <= n; ++i)
      ++cnt[rk[_sa[i]]];
    for (int i = 1; i <= m; ++i)
      cnt[i] += cnt[i - 1];
    for (int i = n; i >= 1; --i)
      sa[cnt[rk[_sa[i]]]--] = _sa[i];
    
    _rk = rk;
    for (int p = 0, i = 1; i <= n; ++i)
      if (_rk[sa[i]] == _rk[sa[i - 1]] && _rk[sa[i] + w] == _rk[sa[i - 1] + w])
        rk[sa[i]] = p;
      else
        rk[sa[i]] = ++p;
  }
  for (int i = 1; i <= n; ++i)
    cout << sa[i] << " \n"[i == n];
  return 0;
}

OI-wiki上还有一些常数优化内容 我懒得研究了ToT..

然后就是看 SA-IS 和 DC3