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

字符串(Updating)

SA

后缀数组。下面可以用 \(i\) 表示 \(s[i\dots n]\)

要记两个东西:\(sa_i, rk_i, sa_i\) 表示后缀排序后第 \(i\) 小的是哪个后缀,\(rk_i\) 表示第 \(i\) 个后缀是第几小的。显然 \(sa[rk[i]]=rk[sa[i]]=i\)

首先考虑 \(\mathcal{O}(n \log^2 n)\) 做法,我们记所有长度为 \(w\) 的子串排序后,第 \(i\) 个子串,是 \(sa_w[i]\)\(s[i\dots i + w - 1]\) 的排名为 \(rk_w[i]\),默认 \(s\) 超过 \(n\) 的部分是空字符。我们让 \(w\)\(1\) 开始,以 \(rk_w[i]\) 为第一关键字,\(rk_w[i+w]\) 为第二关键字排序,这样就可以得到 \(sa_{2w}\)\(rk_{2w}\),当 \(w \ge n\) 时就得到了所有后缀的 \(sa\)\(rk\)。这被称为倍增法。

然后考虑优化,发现两个关键字都很小,直接基数排序,先对第二关键字计数排序,再对第一关键字计数排序,时间复杂度 \(\mathcal{O}(n \log n)\),但是常数还是较大,有如下优化:

  1. 排第二关键字的时候不用计数排序,只要将所有 \(i + w > n\)\(i\) 放在最前面,剩下的位置考虑从小到大枚举 \(i\),若 \(sa_w[i] > w\)\(rk_w[sa_w[i]]\) 可以作为 \(sa_w[i] - w\) 的第二关键字,于是按顺序加入这样的 \(sa_w[i]-w\)
  2. 若一次排序后所有 \(rk\) 已经不同,就已经求出了最后的 \(sa\),直接结束即可。
  3. 每次计数排序的值域可以缩小至上次排序后的 \(rk\) 的最大值。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> pii;
const int N = 1e6 + 10;
template<typename T> void dbg(const T &x) { cout << x << '\n'; }
template<typename Type, typename ...Types> void dbg(const Type &arg, const Types &...args) { cout << arg << ' '; dbg(args...); }
namespace Loop1st {
int n, m, sa[N], id[N], rk[N << 1], oldrk[N << 1], cnt[N];
char s[N];
void main() {cin >> (s + 1);n = strlen(s + 1);for (int i = 1; i <= n; i++) cnt[rk[i] = s[i]]++;m = 127;for (int i = 1; i <= m; i++) cnt[i] += cnt[i - 1];for (int i = n; i; i--) sa[cnt[rk[i]]--] = i;int p = 0;for (int w = 1; w < n; w <<= 1, m = p) {int cur = 0;for (int i = n - w + 1; i <= n; i++) id[++cur] = i;for (int i = 1; i <= n; i++) if (sa[i] > w) id[++cur] = sa[i] - w;memset(cnt, 0, (m + 1) << 2);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];p = 0;memcpy(oldrk, rk, (n + 1) << 2);for (int 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;} if (p == n) break;}for (int i = 1; i <= n; i++) cout << sa[i] << " \n"[i == n];
}}int main() {// freopen("data.in", "r", stdin);// freopen("data.out", "w", stdout);ios::sync_with_stdio(false); cin.tie(0);clock_t Start = clock();int Test = 1;// cin >> Test;for (int tc = 1; tc <= Test; tc++) Loop1st::main();clock_t End = clock();cerr << "Time = "; cerr << fixed << setprecision(6) << 1. * (End - Start) / CLOCKS_PER_SEC << '\n';return 0;
}

另外,SA 还可以用于求 height 数组,定义 \(height[i]=\text{LCP}(sa[i], sa[i - 1]), height[1] = 0\),即两个相邻后缀的 \(\text{LCP}\)。我们有引理:

\[height[rk[i]] \ge height[rk[i - 1]] - 1 \]

证明:\(RHS \le 0\) 时显然成立,当 \(height[rk[i - 1]] > 1\) 时,\(\text{LCP}(sa[rk[i - 1]], sa[rk[i - 1] - 1]) > 1\),即 \(\text{LCP}(i - 1, sa[rk[i-1]-1]) > 1\),我们设这两个后缀的 \(\text{LCP}\)\(aA\),其中 \(a\) 是一个字符,\(A\) 是一个字符串,那么 \(i - 1\) 可以表示为 \(aAD\)\(sa[rk[i-1]-1]\) 可以表示为 \(aAB\),根据 \(sa\) 的性质有 \(B < D\)。而 \(i\) 可以表示为 \(AD\),而 \(sa[rk[i]-1]\)\(rk\) 只比 \(i\)\(1\),且 \(AB < AD\),即 \(AB\)\(rk\)\(i\) 至少小 \(1\),于是有 \(AB \le sa[rk[i]-1] < AD\),所以 $$height[i]=\text{LCP}(i, sa[rk[i]-1]) \ge |A|=height[rk[i-1]]-1$$

\[height[i] \ge height[rk[i-1]]-1 \]

于是可以 \(\mathcal{O}(n)\) 计算。

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

应用

[JSOI2007] 字符加密

\(S\) 复制一遍变为 \(SS\) 之后后缀排序即可。

[USACO07DEC] Best Cow Line G

每次相当于比 \(L\) 开头的后缀和 \(R\) 结尾的前缀,重复不用管因为不重复部分如果比不出来,那随便取哪个,所以将反串接到结尾,中间加个特殊字符跑 SA,然后判断 \(rk\) 即可。

【模版】AC 自动机

子串一定是前缀的后缀,在 \(sa\) 数组上二分并比较即可,比 ACAM 多一个 \(\log |T|\) 的复杂度。

求两子串 LCP

LCP Lemma:
对于 \(i < j < k \le n\),我们有 \(\text{LCP}(i, k) = \min(\text{LCP}(i, j), \text{LCP}(j, k))\)。证明显然,详情可见 许智磊--后缀数组。
LCP Theorem:
\(\text{LCP}(sa[i], sa[j]) = \min\limits_{k=i+1}^j height[k]\)。 证明由 LCP Lemma 直接得出。
于是可以变为 RMQ。

比较两子串大小关系

对于两子串 \(A = s[a\dots b], B = s[c\dots d]\),若 \(\text{LCP}(a, c) \ge \min(|A|, |B|)\)\(A < B \Leftrightarrow |A| < |B|\)。否则,\(A < B \Leftrightarrow rk[a] < rk[c]\)

本质不同子串数目

即后缀的前缀数量减去重复。

考虑按后缀排序的顺序枚举后缀 \(sa[i]\),那么每次新增的子串要除去与上一个串的 \(\text{LCP}\)\(height[i]\)。这个后缀剩下的前缀一定是新增的,如果剩下的前缀中有一个在 \(sa[j]\) 中出现过,那么 \(\text{LCP}(sa[i], sa[j]) > height[i]\),然而 \(\text{LCP}(sa[i], sa[j]) = \min(height[k]) \le height[i]\),矛盾。

所以最终答案就是 \(\dfrac{n(n+1)}{2}-\sum\limits_{i=1}^n height[i]\)

[USACO06DEC] Milk Patterns G

考虑后缀排序,出现的子串一定是连续的 \(k\) 个后缀的前缀,相当于找连续 \(k - 1\)\(height\) 的最小值的最大值,单调队列或 std::multiset 都是可行的。

[ABC141E] Who Says a Pun?

二分答案 \(x\),然后将 \(height\) 数组分为若干 \(\ge x\) 的极长段,然后将每段的 \(sa_i\)\(\min, \max\),减一减看看是否可行即可。

鸽了。

http://www.jsqmd.com/news/583405/

相关文章:

  • 2026年4月市面上铜狮子生产厂家,铜钟/人物雕塑/铜马/铜大缸/关公铜像/铜麒麟/铜大象/铜佛像,铜狮子铸造厂哪家好 - 品牌推荐师
  • 清明节海报设计指南:4个要点打造高级感视觉呈现
  • 让 AI 自己进化自己:深入 HyperAgents
  • 2026年关投强媒体发稿行业口碑分析:真实客户反馈与核心优势测评 - 发稿平台推荐
  • 核心算法与关键技术突破 ——空间计算操作系统的底层原理创新与工程级实现路径
  • FreeRTOS 工程化要点:任务划分、优先级设计与 CPU 占用率监控
  • SIP协议(GB/T 28181)Wireshark抓包内容汇总
  • 文件夹的修改日期可以改吗?分享你三个修改方法
  • 每日温度-leetcode
  • 2026夏天不喜欢穿短裤?透气值和凉感超国标、尺码32个覆盖100斤到290斤——龙牙隐腾到底有多能打? - 行业深度观察
  • 构建nfs provisioner网络存储
  • linux异常报警推送企业微信群聊机器人
  • 穿棕机品牌大比拼:2026年哪些品牌脱颖而出?穿筘机配件/自动穿棕机/穿筘/穿综机配件,穿棕机公司口碑推荐 - 品牌推荐师
  • CLAUDE.md和skill.md有什么不同
  • 2026年市场打印机企业,市场正规的打印机供货商鼎思科技诚信务实提供高性价比服务 - 品牌推荐师
  • 用-ChatGPT-赚钱-使用-AI-轻松在线赚取被动收入的指南
  • 由-ChatGPT-打造的-100-个令人惊叹的电子邮件模板
  • python_12
  • 与机器对话-ChatGPT-和-AI-语言模型的奇妙故事
  • 提升采购效率:如何找到服务完善的液态金属板供应商,防火树脂板/夹植物板/树脂板/PETG装饰板,液态金属板厂家哪个好 - 品牌推荐师
  • 在课堂中使用-ChatGPT-的-80-个方式
  • python3.14t线程池进行并行计算
  • 2026 托福机构排名断层第一!多次元教育如何帮大学生稳拿 90-110 分,直通美日名校 - 速递信息
  • 托福机构哪家靠谱?2026 实测避坑指南 + 5家优质机构推荐(自制力差必看) - 速递信息
  • 干货|xrdp 无人值守+同屏稳定配置(Ubuntu 22.04/24.04 实测可用)
  • 专业人士的-ChatGPT-简化指南
  • 大学生托福备考突围指南:如何选择真正靠谱的培训机构? - 速递信息
  • 作家的-ChatGPT-用-AI-掌握创意写作
  • 网站设计:抓住这3点细节,用户体验感飙升!
  • 基础差怎么选雅思机构|过来人真实避坑与选择建议 - 速递信息