后缀数组学习笔记

写在前面

这篇文章写得比较烂,寒假期间在家里只是简单记了一记,因此之后可能会重构——2020.08.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;

后缀数组学习笔记_第1张图片

求出其中一个就能\(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\)时,子串排序就是后缀排序

后缀数组学习笔记_第2张图片

利用\(rank[1…n][k]\),如何求出\(rank[1…n][k+1]\)

对于两个子串\(sub[i][k+1]\)\(sub[j][k+1]\)
先比较\(rank[i][k]
若相等,再比较\(rank[i+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 
#include 
#include 
#include 
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(按\(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\)会比较相似,比较远的会差别很大

不恰当的例子:

后缀数组学习笔记_第3张图片

优化\(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\)信息即可

你可能感兴趣的:(后缀数组学习笔记)