寫在前面
這篇文章寫得比較爛,寒假期間在家里只是簡單記了一記,因此之后可能會重構——2020.08.05
可能考完 NOIP 之后成績還行的話會重構…… ——2020.11.05
考得不行,不改了…………
- 感謝B站bewildRan老師的講解!
- 感謝OI-Wiki的后綴數組講解!
- 感謝洛谷MaxDYF大佬的博客讓我學會了基數排序!
符號規定
子串
從原串中選取連續的一段即為子串,空串也是子串
后綴
我們用\(suf(k)\)表示\(s(k…n)\)構成的子串
小結論
任何子串都是某個后綴的前綴
最長公共前綴lcp
\(lcp(suf(i), suf(j))\) 表示兩個串\(suf(i)\)和\(suf(j)\)最長的一樣的前綴
問題模型
如何將所有后綴\(,suf(1),suf(2),…,suf(N)\)按照字典序從小到大排序?
方法1
首先看到題目想到的就是直接用暴力,建一個\(cmp\)數組,用\(string\)可以比較大小的性質去暴力\(sort\)
因為\(sort\)是\(n\log n\)的,每次\(cmp\)函數都是\(O(n)\)的,所以總的時間復雜度就是 \(n^2\log n\)
方法2
想一想更好的做法,我們可以用二分+hash
復雜度:\(n \log^2n\)
\(cmp\)函數中二分\(suf(i)\)和\(suf(j)\)的\(lcp\)
\(return\ s[i + |lcp|] < s[j +|lcp|]\)
方法3
\(SA\)算法
$SA[l] = $ 排名第\(l\)的后綴的開始位置
$Rank[i] = $ 后綴\(suf(i)\)的排名
Rank[SA[l]] = l;
SA[Rank[i]] = i;
求出其中一個就能\(O(n)\)求出另一個
有什么求其中一個數組的好的方法呢?
答案是倍增
方法三實現優化
倍增
記\(sub[i][k] = s\)從\(i\)開始長度\(=s^k\)的子串
\(sub[i][k]=s[i…i+(1 << k) - 1]\),超過\(n\)的部分都視為'\0'(字典序最小的字符)
\(rank[i][k]=sub[i][k]\)在長度\(=2^k\)的所有子串中的排名
\(sa[l][k]=\)在長度\(=2^k\)的所有子串中排名第\(l\)的子串的開始位置
過程
- 求出\(,sub[1][0], sub[2][0], …,sub[n][0]\)的字典排序
- 求出\(,sub[1][1], sub[2][1], …,sub[n][1]\)的字典排序
- ……
- 求出\(,sub[1][k], sub[2][k],…,sub[n][k]\)的字典排序
當子串長度\(2^k>=n\)時,子串排序就是后綴排序
利用\(rank[1…n][k]\),如何求出\(rank[1…n][k+1]\)
對於兩個子串\(sub[i][k+1]\)和\(sub[j][k+1]\)
先比較\(rank[i][k]<rank[j][k]\)
若相等,再比較\(rank[i+2^k][k]<rank[j+2^k][k]\)
其實就相當於對二元組\((rank[i][k], rank[i+2^k][k])\)排序
\(pair\)排序時,先按\(first\)比較,若相等再按\(second\)比較
但如果建\(pair\)數組直接\(sort\)的話,復雜度還是\(n\log^2n\),還不如寫二分+hash
於是這個時候就出現了一個神奇的東西:基數排序
為什么可以優化呢?我們注意到\(rank\)這個數組,他的值域是多少?
沒錯,值域就是不超過\(n\)的正整數,所以我們就可以用基數排序,換句話說就是桶排序
關於基數排序的相關,看可以去看一下一位大佬的講解:基數排序
寫\(SA\)時的基數排序用\(cnt\)實現
如何將\(a[i]\)數組基數排序,然后將結果放在\(SA\)數組中呢?
下面的代碼就實現了輸入一個\(a\)數組,得到\(sa\)數組
for (int i = 1; i <= n; i++) ++cnt[a[i]];
for (int i = 1; i <= n; i++) cnt[i] += cnt[i - 1];
for (int i = n; i >= 1; i--) sa[cnt[a[i]]--] = i;
比如一個\(a\)數組為 \(a=[2,1,2,4,2]\)
若用\(sa[l]\)表示排名第\(l\)的數在\(a\)中的下標
則\(sa=[2,1,3,5,4]\)
就可以根據
Rank[SA[l]] = l;
SA[Rank[i]] = i;
得出\(rank\)數組\(rank=[2,1,2,3,2]\)
到這里我們就能回到一開始的問題,實現用\(rank[1…n][k]\),如何求出\(rank[1…n][k+1]\),步驟如下:
\(for(k = 1 \sim \log n)\)
- 按\(rank[i+2^k][k]\)(第二關鍵字)基數排序
- 按\(rank[i][k]\)(第一關鍵字)基數排序,得到\(sa[i][k+1]\)數組
- 由\(sa[i][k+1]\)求出\(rank[i][k+1]\)
如果你細心的話可能會發現,\(k\)是從\(1\)開始的而不是從\(0\)開始的,那么\(k\)是\(0\)時候怎么來的呢?
因為\(2^0\)就是\(1\),所以我們可以直接把\(rank\)數組(也就是排名)先設成當前字符的\(\texttt{ASCII}\)碼,這樣就可以啦~
sa->rank
如果\(rk[i]\)中有並列
for (int p = 0, i = 1; i <= n; i++) {
if(oldrk[sa[i]] == oldrk[sa[i - 1]] && oldrk[sa[i] + k] == oldrk[sa[i - 1] + k])
rk[sa[i]] = p;
else rk[sa[i]] = ++p;
}
代碼
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int A = 1e6 + 11;
inline int read() {
char c = getchar();
int x = 0, f = 1;
for ( ; !isdigit(c); c = getchar()) if (c == '-') f = -1;
for ( ; isdigit(c); c = getchar()) x = x * 10 + (c ^ 48);
return x * f;
}
char s[A];
int n, m, sa[A], rank[A], tp[A], tax[A];
void cntsort() {
for (int i = 0; i <= m; i++) tax[i] = 0;
for (int i = 1; i <= n; i++) tax[rank[i]]++;
for (int i = 1; i <= m; i++) tax[i] += tax[i - 1];
for (int i = n; i >= 1; i--) sa[tax[rank[tp[i]]]--] = tp[i];
}
void Sort() {
m = 75;
for (int i = 1; i <= n; i++) rank[i] = s[i] - '0' + 1, tp[i] = i;
cntsort();
for (int w = 1, p = 0; p < n; m = p, w <<= 1) {
p = 0;
for (int i = 1; i <= w; i++) tp[++p] = n - w + i;
for (int i = 1; i <= n; i++) if(sa[i] > w) tp[++p] = sa[i] - w;
cntsort();
swap(tp, rank);
rank[sa[1]] = p = 1;
for (int i = 2; i <= n; i++) {
rank[sa[i]] = (tp[sa[i - 1]] == tp[sa[i]] && tp[sa[i - 1] + w] == tp[sa[i] + w]) ? p : ++p;
}
}
}
int main() {
scanf("%s", s + 1);
n = strlen(s + 1);
Sort();
for(int i = 1; i <= n; i++) cout << sa[i] << ' ';
return 0;
}
Height數組
我們通過求\(SA\)數組可以把所有后綴排序,那么排序之后有啥用呢??
其實是為了快速的求出任意兩個后綴的\(lcp\)長度
我們記\(Height[l]=\)排名第\(l-1\)的后綴和排名第\(l\)的后綴的\(lcp\)長度
\(Height[l] = lcp(suf(SA[l-1], suf(SA[l])))\)
其中\(Height[1]\)可以視作\(0\)。
假設\(l=\)后綴\(suf(i)\)的排名,\(r=\)后綴\(suf(j)\)的排名(在此\(l\)不一定小於\(r\),只是舉例),那么有結論:
- \(lcp(suf(i),duf(j))=min(Height[l+1]…Height[r])\)
- 即兩個后綴的\(lcp=\)它們排名區間中\(Height\)的最小值
可以用數據結構維護\(rmp\)
為什么可以這么理解呢?
假設有三個字符串\(s_1,s_2,s_3\),且\(s_1<s_2<s_3\)(按\(rank\)排名得出)
那么\(lcp(s_1,s_3)\)就等於\(min(lcp(s_1,s_2), lcp(s_2,s_3))\)
(詳細證明需要畫圖……我真的懶)
\(lcp(s_1,s_3) >= min(lcp(s_1,s_2), lcp(s_2,s_3))=1\)
又有\(s_1[l+1]!= s_3[l+1]\)
求法
那么如何快速求出\(Height\)數組呢?
純暴力\(O(n^2)\)
for i = 1 - N
l = rank[i]
j = sa[l - 1]
k = 0
while (s[i + k] ==s [j + k]): ++k
Height[l] = k
令\(l = rank[i], r = rank[i-1]\)
\(Height[l] = lcp(suf(SA[l-1]), suf(i))\)
\(,Height[r] = 1cp(suf(SA[r-1]),suf(i-1))\)
有重要結論:
\(Height[l] >= Height[r] - 1\)
- 若\(Height[r]>1\),有\(suf(SA[r-1]) < suf(SA[i-1])\)
- 去掉首個字符 \(,lcp(suf(SA[r-1]+1), suf(SA[i])) = Height[r] - 1\)
- \(suf(SA[r-1]+1) < suf(SA[i])\)
- 由於$Height[1] \(是\)suf(i)\(與排名緊挨着自己的后綴的\)lcp$,有
- \(suf(SA[r-1]+1) <= suf(SA[1-1]) < suf(SA[i])\)
相近的\(Height\)會比較相似,比較遠的會差別很大
不恰當的例子:
優化\(O(n)\)
利用\(Height[rank[i]] >= Height[rank[i-1] ] - 1\)
優化暴力即可,復雜度\(O(N)\)
for i = 1 - N
j = sa[l - 1]
k = max(0, Height[rank[i - 1]] - 1)
while (s[i + k] == S[j+k]): ++k
Height[rank[i]] = k
之后再用\(st\)表之類的維護\(Height\)的\(rmq\)信息即可