首页 > 其他分享 >后缀数组学习笔记

后缀数组学习笔记

时间:2023-10-02 09:05:09浏览次数:33  
标签:后缀 笔记 int kMaxN 数组 sa 排序 rk

  • 基数排序

利用桶的单调性,从低位到高位依次将整数放到对应数位的桶中。

  • 后缀数组

定义:对于字符串 \(s\),定义 \(sa[i]\) 表示 \(s\) 的 \(n\) 个后缀按字典序排序后的第 \(i\) 个后缀在 \(s\) 中的下标,\(rk[i]\) 表示从 \(s_i\) 开始的后缀在后缀数组中的下标。

  • 倍增求 \(sa\):

不妨设 \(sa_{w,i}\) 表示只取每个后缀的前 \(w\) 个字符排序后第 \(i\) 个后缀在 \(s\) 中的下标。

考虑通过 \(sa_w\) 求出 \(sa_{2w}\)。

容易发现如果 \(rk_{w,i}<rk_{w,j}\) 那么 \(s[i,\dots,i+w-1]\) 一定小于 \(s[j,\dots,j+w-1]\)。

所以可以把每个长度为 \(2w\) 的子串拆成两个长度为 \(w\) 的子串。

那么判断两个长度为 \(2w\) 的子串 \(s[i,\dots,i+2w-1]\) 和 \(s[j,\dots,j+2w-1]\) 的字典序就只要判断 \(rk_{w,i},rk_{w,i+w},rk_{w,j},rk_{w,j+w}\) 之间的关系。

如果 \(i\) 比 \(j\) 小,那么 \(rk_{w,i}<rk_{w,j}\) 或者 \(rk_{w,i}=rk_{w,j}\) 并且 \(rk_{w,i+w}<rk_{w,j+w}\)。

所以可以直接双关键字排序。

时间复杂度:\(O(n\log^2 n)\)。

代码
#include <bits/stdc++.h>

// #define int int64_t

const int kMaxN = 1e6 + 5;

int n;
int sa[kMaxN << 2], rk[kMaxN << 2], nrk[kMaxN << 2];
std::string s;

void dickdreamer() {
  std::cin >> s;
  n = s.size();
  s = " " + s;
  for (int i = 1; i <= n; ++i) {
    sa[i] = i;
    rk[i] = s[i];
  }
  for (int w = 1; w <= n; w <<= 1) {
    auto cmp = [&] (const int x, const int y) {
      return rk[x] == rk[y] ? rk[x + w] < rk[y + w] : rk[x] < rk[y];
    };
    std::sort(sa + 1, sa + 1 + n, cmp);
    int c = 0;
    for (int i = 1; i <= n; ++i)
      nrk[sa[i]] = (rk[sa[i]] == rk[sa[i - 1]] && rk[sa[i] + w] == rk[sa[i - 1] + w] ? c : ++c);
    for (int i = 1; i <= n; ++i)
      rk[i] = nrk[i];
  }
  for (int i = 1; i <= n; ++i)
    std::cout << sa[i] << ' ';
}

int32_t main() {
#ifdef ORZXKR
  freopen("in.txt", "r", stdin);
  freopen("out.txt", "w", stdout);
#endif
  std::ios::sync_with_stdio(0), std::cin.tie(0), std::cout.tie(0);
  int T = 1;
  // std::cin >> T;
  while (T--) dickdreamer();
  // std::cerr << 1.0 * clock() / CLOCKS_PER_SEC << "s\n";
  return 0;
}
  • 优化

容易发现像上面那样求 \(rk\),每个 \(rk\) 的值域是 \([1,n]\),所以直接先对第二关键字排序,再对第一关键字排序即可。

时间复杂度:\(O(n\log n)\)。

代码
void suffix_sort(std::string s, int *sa, int *rk) {
  static int cnt[kMaxN], ork[kMaxN << 1], id[kMaxN];
  memset(cnt, 0, sizeof(cnt));
  int n = static_cast<int>(s.size()) - 1;
  for (int i = 1; i <= n; ++i) {
    rk[i] = s[i];
    ++cnt[rk[i]];
  }
  for (int i = 1; i <= 128; ++i)
    cnt[i] += cnt[i - 1];
  for (int i = n; i; --i)
    sa[cnt[rk[i]]--] = i;
  for (int i = 1; i <= n; ++i)
    ork[i] = rk[i];
  int m = 0;
  for (int i = 1; i <= n; ++i) {
    if (ork[sa[i]] == ork[sa[i - 1]]) {
      rk[sa[i]] = m;
    } else {
      rk[sa[i]] = ++m;
    }
  }
  for (int w = 1; w < n; w <<= 1) {
    memset(cnt, 0, sizeof(cnt));
    for (int i = 1; i <= n; ++i)
      id[i] = sa[i];
    for (int i = 1; i <= n; ++i)
      ++cnt[rk[id[i] + w]];
    for (int i = 1; i <= m; ++i)
      cnt[i] += cnt[i - 1];
    for (int i = n; i; --i)
      sa[cnt[rk[id[i] + w]]--] = id[i];

    memset(cnt, 0, sizeof(cnt));
    for (int i = 1; i <= n; ++i)
      id[i] = sa[i];
    for (int i = 1; i <= n; ++i)
      ++cnt[rk[id[i]]];
    for (int i = 1; i <= m; ++i)
      cnt[i] += cnt[i - 1];
    for (int i = n; i; --i)
      sa[cnt[rk[id[i]]]--] = id[i];
    
    for (int i = 1; i <= n; ++i)
      ork[i] = rk[i];
    m = 0;
    for (int 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]] = m;
      } else {
        rk[sa[i]] = ++m;
      }
    }
  }
}

但是这么做常数很大,并且实测还没直接 sort 快,因为做两次基数排序是很慢的。

注意到第一次排序相当于把 \(i+w>n\) 的 \(i\) 放前面,并且把其他的 \(i\) 按照 \(rk[i+w]\) 的顺序排序。

又因为原来的 \(sa\) 数组就已经按照 \(rk\) 排好序了,所以直接从前往后扫 \(sa\) 数组,如果 \(sa[i]>w\) 就把 \(sa[i]-w\) 放到新数组中即可。

还有个小优化是如果当前的 \(rk\) 总共出现 \(n\) 次就说明已经排序完成,那么 break 就可以了。

这样做就比直接 sort 要快很多了。

代码
void getsa(std::string s, int *sa, int *rk) {
  static int cnt[kMaxN], ork[kMaxN << 1], id[kMaxN];
  memset(cnt, 0, sizeof(cnt));
  int n = static_cast<int>(s.size()) - 1, m = 0;
  for (int i = 1; i <= n; ++i) {
    rk[i] = s[i];
    ++cnt[rk[i]];
  }
  for (int i = 1; i <= 128; ++i)
    cnt[i] += cnt[i - 1];
  for (int i = n; i; --i)
    sa[cnt[rk[i]]--] = i;
  std::copy_n(rk + 1, n, ork + 1);
  for (int i = 1; i <= n; ++i) {
    if (ork[sa[i]] == ork[sa[i - 1]]) {
      rk[sa[i]] = m;
    } else {
      rk[sa[i]] = ++m;
    }
  }
  for (int w = 1; m < n; w <<= 1) {
    int p = 0;
    for (int i = n - w + 1; i <= n; ++i)
      id[++p] = i;
    for (int i = 1; i <= n; ++i)
      if (sa[i] > w)
        id[++p] = sa[i] - w;
    std::fill_n(cnt + 1, n, 0);
    for (int i = 1; i <= n; ++i)
      ++cnt[rk[id[i]]];
    for (int i = 1; i <= m; ++i)
      cnt[i] += cnt[i - 1];
    for (int i = n; i; --i)
      sa[cnt[rk[id[i]]]--] = id[i];
    
    m = 0;
    std::copy_n(rk + 1, n, ork + 1);
    for (int 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]] = m;
      } else {
        rk[sa[i]] = ++m;
      }
    }
  }
}

\(height\) 数组

定义:\(sa[i-1]\) 和 \(sa[i]\) 的最长公共前缀长度。

求 \(height\)

  • 引理:\(height[rk[i]]\geq height[rk[i-1]]-1\)。
证明

\(height[rk[i]]\) 就是 \(s_i\) 与 \(sa\) 中 \(i\) 前面的后缀的 LCP。

标签:后缀,笔记,int,kMaxN,数组,sa,排序,rk
From: https://www.cnblogs.com/Scarab/p/17739681.html

相关文章

  • 5. 数组
    1.数组的概述1.1数组的概念数组(Array),是多个相同类型数据按一定顺序排列的集合,并使用一个名字命名,并通过编号的方式对这些数据进行统一管理数组中的概念数组名下标(索引)元素数组的长度​​‍数组的特点:数组本身是引用数据类型​,而数组中的元素可......
  • 迷失岛2笔记3 场景数据保存
     首先我们注册一下这个场景前后数据变化的事件 然后在我们切换场景这里做一个场景前场景后的一个数据切换 事件调用 然后再里面写逻辑   可以看到上面的字典他是一个枚举和一个Bool 就是用这个来判断哪些物品是否要激活使用过   他就是查找当前场......
  • 20211301 学习笔记4
    学习笔记4教材知识总结7.1文件操作级别文件操作:分为5个级别(从高到低如下)硬件级别:fdisk(硬盘、U盘、sdc盘分区mkfs:格式化磁盘分区,为系统做好准备fsck:检查和维修系统碎片整理:压缩文件系统中的文件操作系统内核中的文件系统函数系统调用I/O库函数用户命令......
  • 学习笔记4
    学习笔记:文件操作知识点归纳文件操作级别硬件级别fdisk:将硬盘、U盘或SDC盘分区mkfs:格式化磁盘分区,为系统做好准备fsck:检查和维修系统碎片整理:压缩文件系统中的文件操作系统内核中的文件系统函数系统调用I/O库函数用户命令sh脚本文件I/O操作低级别文件操作分区低......
  • 《信息安全系统设计与实现》第四周学习笔记
      第七章文件操作级别:硬件级别fdiskmkfsfsck碎片整理操作系统内核中的文件系统函数:系统调用I/O库函数用户命令sh脚本低级别文件操作:分区Command(mforhelp):m---输出帮助信息Commandactionatoggleabootableflag---设置启动分区beditbsddisklabe......
  • Webpack5 基础使用笔记
    [webpack中文文档](概念|webpack中文文档|webpack中文文档|webpack中文网(webpackjs.com)):本质上,webpack是一个用于现代JavaScript应用程序的静态模块打包工具。当webpack处理应用程序时,它会在内部从一个或多个入口点构建一个依赖图(dependencygraph),然后将你......
  • 第七、八章学习笔记
    学习笔记第七章文件操作2023-10-011.文件操作级别(1)硬件级别:fdisk:将硬件、U盘或SDC盘分区。mkfs:格式化磁盘分区,为系统做好准备。fsck:检查和维修系统。碎片整理:压缩文件系统中的文件。(2)操作系统中的文件系统函数(3)系统调用(4)I/O库函数(5)用户命令(6)sh......
  • 后缀数组
    基数排序算法思想:利用桶的单调性,从低到高位依次将整数放进对应数位的桶中。时间复杂度:\(O(d*(n+siz))\),其中\(d\)为数位,\(n\)为元素个数,\(siz\)为桶的大小。后缀树对于字符串\(s\),取出\(s\)所有的后缀字串,并建立字典树。这个树就是\(s\)的后缀树。空间复杂度\(......
  • 学习笔记(4)
    一、任务详情自学教材第7,8章,提交学习笔记(10分),评分标准如下知识点归纳以及自己最有收获的内容,选择至少2个知识点利用chatgpt等工具进行苏格拉底挑战,并提交过程截图,提示过程参考下面内容(4分)问题与解决思路,遇到问题最先使用chatgpt等AI工具解决,并提供过程截图(3分)实践过程截图,......
  • 斜率优化学习笔记
    下面可能会用到一些叫上凸壳鸭下凸壳呀的名词,大家不用弄懂它的概念的说也不用望风而逃(我之前就望风而逃过(*/ω\*)),凸壳就简单理解成一个凸了一块的一段连线就好了。斜率优化有些地方不好用代数的语言刻画,比如下凸壳,比如凸包(凸包有的定义就直接写橡皮筋的比喻233333),以及为什么下凸......