后缀数组(SA) 学习笔记
天梯赛校赛碰到的 本以为切个水题 没想到捅了老挝
甚至还有n,q=10w的区间众数 2333 就是数据比较水 感觉严一点分块都不一定能过
原题貌似是这个 但是他妈的校赛n改到了2w
一开始感觉是差分+lcs或kmp之类的 然后感觉不太对劲...
还是需要一些处理字符串的手段 于是学一下SA和SAM 已经拖了挺久了...
前面的定义和求解过程理解移步oi-wiki即可
那上面有图解,看的更清楚
定义
对于一长为的字符串 即 令 称为后缀
将的个后缀排序
- 表示第小的后缀的编号 即是第小的后缀
- 表示后缀的排名
即
求后缀数组
求法很显然。直接暴力排序就行了
考虑两个连续长为1的子串 即和的排名和作为两个关键字
能够得到所有长为2的子串的 通过得到 然后再求
以此类推得到表示子串的后缀数组
整个过程就是用的子串排名即数组去求解 的子串的后缀数组
能够倍增的原因就是因为连续的字符串排字典序 前面优先级大于后面 折半做就很显然
当时即为整个串的后缀数组
#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 中间还有个更新操作 就是的复杂度
呃呃 然后按理说是过不去的 但是C++17开了O2居然就直接过了... 过了...
但是语言与时俱进的同时 丧心病狂的出题人肯定也会与时俱进 所以我们还是要学习标准算法的 233
复杂度降到
计数排序和基数排序 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