将字符串 \(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]\):
- 将 \(\text{rk}_w[i]\) 作为第一关键字,\(\text{rk}_w[i+2^w]\) 作为第二关键字进行排序,存到 \(\text{sa}\) 里。若 \(i+2^w > n\),可视 \(\text{rk}_{w}[i+2^w]\) 为 \(0\),即最小。
- 通过 \(\text{sa}\) 反推出 \(\text{rk}_{w+1}[i]\)
- 不断重复上述过程,直到 \(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]]\):
将这两个字符串的第一个字符去掉:
橙色部分是可能多匹配的 \(\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]\)