当前位置: 首页 > news >正文

后缀数组基础 Suffix Array

将字符串 \(s\) 的所有后缀按字典序排序。

SA 算法主要求以下数组:

  • \(\text{sa}_i\):排名为 \(i\) 的后缀的下标。

  • \(\text{rk}_i\):下标以 \(i\) 开始的排名。

  • \(\text{ht}_i\)\(\text{height}\) 数组。

    • \(\text{ht}_1 = 0,\text{ht}_i=\text{lcp}(\text{sa}[i],\text{sa}[i-1])\)\(\text{lcp}(i,j)\) 表示后缀 \(i\) 和后缀 \(j\) 的最长公共前缀的长度。
    • 即第 \(i\) 小的后缀与第 \(i-1\) 小的后缀的最长公共前缀的长度。

求 sa 和 rk

\(n = |s|\)

根据定义有 \(\text{sa}[\text{rk}[i]]=\text{rk}[\text{sa}[i]] = i\)

考虑倍增求:

\(\text{rk}_w[i]\) 为字符串 \(s[i:i+2^w-1]\) 在所有长度为 \(2^w\)\(s\) 的字串的排名。

可设 \(\text{rk}_0[i] = s[i]\)(此时是相对排名)。

设已经求出所有 \(\text{rk}_w[i]\),考虑求 \(\text{rk}_{w+1}[i]\)

  1. \(\text{rk}_w[i]\) 作为第一关键字,\(\text{rk}_w[i+2^w]\) 作为第二关键字进行排序,存到 \(\text{sa}\) 里。若 \(i+2^w > n\),可视 \(\text{rk}_{w}[i+2^w]\)\(0\),即最小。
  2. 通过 \(\text{sa}\) 反推出 \(\text{rk}_{w+1}[i]\)
  3. 不断重复上述过程,直到 \(2^w > n\)

最多会倍增 \(O(\log n)\) 次,每次排序的时间复杂度为 \(O(n\log n)\),总时间复杂度为 \(O(n\log^2 n)\)

考虑排序用基数排序 + 计数排序,这样每次排序的时间复杂度为 \(O(n)\),总时间复杂度为 \(O(n\log n)\)

/* OI wiki */
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <iostream>using namespace std;constexpr int N = 1000010;char s[N];
int n, sa[N], rk[N << 1], oldrk[N << 1], id[N], cnt[N];int main() {int i, m, p, w;scanf("%s", s + 1);n = strlen(s + 1);m = 127;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;memcpy(oldrk + 1, rk + 1, n * sizeof(int));for (p = 0, i = 1; i <= n; ++i) {if (oldrk[sa[i]] == oldrk[sa[i - 1]]) {rk[sa[i]] = p;} else {rk[sa[i]] = ++p;}}for (w = 1; w < n; w <<= 1, m = n) {// 对第二关键字:id[i] + w进行计数排序memset(cnt, 0, sizeof(cnt));memcpy(id + 1, sa + 1,n * sizeof(int));  // id保存一份儿sa的拷贝,实质上就相当于oldsafor (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];// 对第一关键字:id[i]进行计数排序memset(cnt, 0, sizeof(cnt));memcpy(id + 1, sa + 1, n * sizeof(int));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];memcpy(oldrk + 1, rk + 1, n * sizeof(int));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;}}}for (i = 1; i <= n; ++i) printf("%d ", sa[i]);return 0;
}

但是上面代码常数太大,考虑进行一些常数优化。

  • 第二关键字排序优化

    其实上一轮的 \(\text{sa}\) 已经求出 \(\text{rk}_w[i+2^w]\) 的顺序,直接用就行。省略的对第二关键字的计数排序。

  • 计数排序值域优化

    每轮的排名都没排到 \(n\),记录一个 \(m\) 表示当前最大的排名,可以减少计数排序的常数。

  • 倍增次数优化

    若当前的排名排到了 \(n\),说明所有的后缀只用前 \(2^{w+1}\) 个字符就确定了顺序,再往后比较就没有意义了,此时结束算法即可。

最终代码:空间复杂度 \(O(n)\),时间复杂度\(O(n\log n)\),常数小

const int N = 1e6 + 5;
char s[N];
int n, sa[N], rk[N], tp[N], cnt[N];
void SA() {int m = 127;rep(i, 1, n) cnt[rk[i] = s[i]]++;rep(i, 1, m) cnt[i] += cnt[i - 1];per(i, 1, n) sa[cnt[rk[i]]--] = i;for(int w = 1, p; ; w <<= 1, m = p /* 计数排序值域优化 */){/* 第二关键字排序优化 */p = 0;rep(i, n - w + 1, n) tp[++p] = i;rep(i, 1, n) if(sa[i] > w) tp[++p] = sa[i] - w;/* 对第一关键字排序 */memset(cnt, 0, sizeof(cnt));rep(i, 1, n) cnt[rk[i]]++;rep(i, 1, m) cnt[i] += cnt[i - 1];per(i, 1, n) sa[cnt[rk[tp[i]]]--] = tp[i];/* 用 sa 反推 rk */memcpy(tp, rk, sizeof(tp));rk[sa[1]] = p = 1;rep(i, 2, n) rk[sa[i]] = (tp[sa[i]] == tp[sa[i - 1]] && tp[min(sa[i] + w, n + 1)] == tp[min(sa[i - 1] + w, n + 1)]) ? p : ++p;if(p == n) break; /* 倍增次数优化 */}
}

求 height 数组

引理:\(\text{ht}[\text{rk}[i]] \ge \text{ht}[\text{rk}[i-1]]-1\)

证明:

\(\text{ht}[\text{rk}[i-1]] \le 1\),原式显然成立。

\(\text{ht}[\text{rk}[i-1]] > 1\)

考虑 \(\text{ht}[\text{rk}[i-1]]\)

height_array_1.png

将这两个字符串的第一个字符去掉:

height_array_2.png

橙色部分是可能多匹配的 \(\text{lcp}\)

因为后缀 \(\text{sa}[\text{rk}[i-1]-1]\) 和后缀 \(i - 1\)\(\text{lcp}\) 长度 \(>1\) ,则去掉一个字符后,后缀 \(\text{sa}[\text{rk}[i-1]+1]\) 还是还是排在后缀 \(i\) 前面。

后缀 \(\text{sa}[\text{rk}[i]-1]\) 是排在后缀 \(i\) 的前一个,又因为这是按字典序排的,所以后缀 \(\text{sa}[\text{rk}[i]-1]\) 和后缀 \(i\)\(\text{ht}[\text{rk}[i-1]]-1\) 个字符必定相同,即 \(\text{ht}[\text{rk}[i]] \ge \text{ht}[\text{rk}[i-1]]-1\)

证毕。

代码实现:时间复杂度 \(O(n)\)

for(int i = 1, k = 0; i <= n; i++)
{k -= !!k;while(s[i + k] == s[sa[rk[i] - 1] + k]) k++;ht[rk[i]] = k;
}

应用:

1. 求两个后缀的最大 \(\text{lcp}\) 长度\(\text{lcp}(\text{sa}[i],\text{sa}[j]) = \min\limits_{i\le k\le j}\{\text{ht}[k]\}\),可以结合树状数组。

2. 比较两个字串字典序大小:比较 \(A=s[a:b]\)\(B=s[c:d]\)

  • \(\text{lcp}(a,c) \ge \min(|A|,|B|), A < B \Leftrightarrow |A| < |B|\)\(A\)\(B\) 的前缀或 \(B\)\(A\) 的前缀。

  • \(\text{lcp}(a,c) < \min(|A|,|B|), A < B \Leftrightarrow \text{rk}[a] < \text{rk}[c]\)

http://www.hskmm.com/?act=detail&tid=14333

相关文章:

  • @Param的作用
  • 后端应该对前端数据来源渠道进行验证
  • 思念比爱更深刻
  • 数据库操作的方法签名
  • 完整教程:第33章 AI在教育领域的应用
  • 易软通openWMS - 功能齐全的开源WMS
  • C# 中的 ReferenceEquals 方法 - 教程
  • 遇到一件循环导入事件
  • flask实现后端接口的封装和开发部分
  • 上海这样的地段简直是逆天
  • 【GitHub每日速递 250923】 Google 又放大招!TimesFM 2.5 参数减半,预测更准更快
  • 具身智能机器人架构:人形机器人系统架构深度拆解
  • 卓驭,欧洲无绝境
  • 下周审核4家IPO,2家再融资。其中两家IPO企业于在审期间调减募资规模
  • 280亿国产AI独角兽,惹怒“地表最强法务部”
  • 读人形机器人20财富再分配
  • Java 与人工智能的深度融合:从数据到推理服务
  • Java 与大数据实时处理:Kafka、Flink 与企业应用
  • Java 与企业级中间件:消息、缓存与数据库集成
  • 基于 Vite7 与 Vue3 的 WebOS 后台系统架构实践
  • 啊哈哈20250923_03:23
  • js获取浏览器指纹
  • gitIgnore 无法忽略dist目录的变更
  • 翻转二叉树
  • 我的第一篇博客
  • 测试测试测试测试测试
  • java中的浮点数计算
  • XYCTF2025复现(WEB)
  • 洛谷 P13973 [VKOSHP 2024] Nightmare Sum
  • 发布/订阅(Publish/Subscribe)与交换机(Exchange)