「筆記」后綴數組


寫在前面

這篇文章寫得比較爛,寒假期間在家里只是簡單記了一記,因此之后可能會重構——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\)的子串的開始位置

過程

  1. 求出\(,sub[1][0], sub[2][0], …,sub[n][0]\)的字典排序
  2. 求出\(,sub[1][1], sub[2][1], …,sub[n][1]\)的字典排序
  3. ……
  4. 求出\(,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\)信息即可


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM