后缀数组学习笔记

用途

高效组织字符串中的所有子串。


思想

“所有前缀的所有后缀即为所有子串。”


概念定义

  • sa:suffix array,即后缀数组。表示字符串所有非空后缀按字典序从小到大排序后,后缀们的第一个字符在原串中的下标。

  • rk:该字符开头的后缀,在 sa 中排名第几。

  • sa 和 rk 的转化:显然,当 rk 互不相同时,满足 sa[rk[i]] = irk[sa[i]] = i;但 rk 在我们的算法过程中是有可能相同的(尽管结果上是没有可能的),这时就需要注意了。


建立过程

众所周知直接 sort 所有后缀是 \(O(n^2 \log n)\) 的复杂度。这里复杂度之所以差,是因为实际后缀是有大量重复部分的。能不能充分利用每一次比较呢?

有一个想法是利用 较短 字串的比较得到 较长 字串的比较。听到这里,应该不难想到试着用 倍增法 来扩大字串长度:用两个长度为 1 的字串的比较结果得到长度为 2 的字串的比较结果,用两个长度为 2 的字串的比较结果得到长度为 4 的字串的比较结果……

引自 OIwiki

那具体该怎么利用两个较短字串的结果呢?假设当前正在对长度为 \(w\) 的字串排序,当前字串为 \([i, i+w-1]\),它可以平分成等长的左右两个子区间,而这左右两个子区间的排名在上一步已经确定了。于是便可以以左区间排名为第一关键字,以右区间排名为第二关键字,对这 \(n\) 个长度为 \(w\) 的区间排序(关于右区间超出 \(n\) 的区间,就当右区间为排名为 0 即可)。

那么可以初步建立一个算法:

  • 初始化 \(sa[i] = i, rk[i] = s[i]\)

  • 开始倍增。先对 sa 按照上述方法排序。

  • 用 sa 更新 rk。注意,这里的 rk 就有可能出现相等的情况,因此需要判断一下 \(sa[i]\)\(sa[i-1]\) 对应字串是否相等。

代码如下:

#include<bits/stdc++.h>
using namespace std;

const int MAXN = 1e6+5;
int n, m, w, rk[MAXN<<1], tmp[MAXN], sa[MAXN];
char s[MAXN];

inline bool cmp(int x, int y){
	if(rk[x] != rk[y])	return rk[x] < rk[y];
	return rk[x+w] < rk[y+w];
}

int main(){
	scanf("%s", s+1);
	n = strlen(s+1);
	for(int i = 1; i <= n; i++)	sa[i] = i, rk[i] = s[i];
	
	for(w = 1; w < n; w <<= 1){
		sort(sa+1, sa+1+n, cmp);
		for(int i = 1; i <= n; i++)
			tmp[sa[i]] = tmp[sa[i-1]]+(cmp(sa[i-1], sa[i]) ? 1 : 0);//避免 rk 值相同 
		for(int i = 1; i <= n; i++)	rk[i] = tmp[i];
	}
	for(int i = 1; i <= n; i++)		printf("%d ", sa[i]);
	
	return 0;
}

因为使用了倍增 + sort,时间复杂度为 \(O(n \log^2 n)\)

这是还有优化空间的。注意到 rk 的范围在 \(1 \sim n\) 之间,那么可以使用 桶排 将单次排序优化到 \(O(n)\);由于这里有两个关键字,因此还要使用 基数排序

具体来说,每次长度的排序大致如下:

  • 先对第二关键字(rk[i+w])进行排序。

  • 引入一个辅助数组——标记桶。它记录的是“每个关键字的排名”。当然,我们可以先求得“每个关键字的数量”,再求前缀和,即可求得排名。

  • 更新 sa。由于 sa 互不相同,代码中对标记桶有一个巧妙的处理:每次访问对应标记桶,令标记桶 --

  • 对第一关键字(rk[i])进行排序,过程大同小异。但是在更新 sa 时需要额外注意一点:在第一关键字相同时,要尽量保留第二关键字的排序结果。 因此代码中记录了一个 last_sa 并用了一个倒序更新,请结合代码理解。

  • 最后,对 rk 进行更新。方法与倍增 + sort 是一样的。

代码如下:

#include<bits/stdc++.h>
using namespace std;

const int MAXN = 1e6+5;
int n, m, bjt[MAXN<<1], rk[MAXN<<1], sa[MAXN], lrk[MAXN<<1], lsa[MAXN];
char s[MAXN];

inline bool cmp(int x, int y, int i){
	if(lrk[x] != lrk[y])	return lrk[x] < lrk[y];
	return lrk[x+i] < lrk[y+i];
}

int main(){
	scanf("%s", s+1);
	n = strlen(s+1);
	
	int w = 127;//w 为桶的值域
	for(int i = 1; i <= n; i++)	rk[i] = s[i], ++bjt[rk[i]];
	for(int i = 1; i <= w; i++)	bjt[i] += bjt[i-1];
	for(int i = 1; i <= n; i++)	sa[bjt[rk[i]]--] = i;
	for(int i = 1; i <= n; i++)	lrk[i] = rk[i];
	for(int i = 1; i <= n; i++)
		rk[sa[i]] = rk[sa[i-1]]+(lrk[sa[i]] == lrk[sa[i-1]] ? 0 : 1);
	
	w = n<<1;
	//为什么是 < n?1+n-1 = n 
	for(int i = 1; i < n; i <<= 1){
		//对第二关键字进行排序
		for(int j = 0; j <= w; j++)	bjt[j] = 0;
		for(int j = 1; j <= n; j++)	++bjt[rk[j+i]];
		for(int j = 1; j <= w; j++)	bjt[j] += bjt[j-1];
		for(int j = 1; j <= n; j++)	sa[bjt[rk[j+i]]--] = j;
		//对第一关键字进行排序
		for(int j = 0; j <= w; j++)	bjt[j] = 0;
		for(int j = 1; j <= n; j++)	lsa[j] = sa[j];
		for(int j = 1; j <= n; j++)	++bjt[rk[j]];
		for(int j = 1; j <= w; j++)	bjt[j] += bjt[j-1];
		//倒序循环:由于是 --,要使原本大的尽量大(保留上次排序结果) 
		for(int j = n; j >= 1; j--)	sa[bjt[rk[lsa[j]]]--] = lsa[j];
		//更新 rk 
		for(int j = 1; j <= n; j++)	lrk[j] = rk[j];
		for(int j = 1; j <= n; j++)
			rk[sa[j]] = rk[sa[j-1]]+(cmp(sa[j-1], sa[j], i) ? 1 : 0);
	}
	for(int i = 1; i <= n; i++)	printf("%d ", sa[i]);
	
	return 0;
}
//这里代码颜色渲染是怎么回事???

如此,总复杂度为 \(O(n \log n)\)


卡常技巧

据说这是 OIwiki 上两个最有用的。

1. 第二关键字无需基数排序

思考一下第二关键字排序的实质,其实就是把超出字符串范围(即 sa[i] + w > n)的 sa[i] 放到 sa 数组头部,然后把剩下的依原顺序放入。

这段话说得较为清楚。但是,注意我们实际是按照 \(sa[j] = sa[i]+w\) 的位置关系对 sa 排序,不要把 \(sa[i]\) 本身的位置关系与此弄混。

2. 若排名都不相同可直接生成后缀数组

#include<bits/stdc++.h>
using namespace std;

const int MAXN = 1e6+5;
int n, m, bjt[MAXN], rk[MAXN], sa[MAXN], lrk[MAXN<<1], lsa[MAXN];
char s[MAXN];

inline bool cmp(int x, int y, int i){
	if(lrk[x] != lrk[y])	return lrk[x] < lrk[y];
	return lrk[x+i] < lrk[y+i];
}

int main(){
	scanf("%s", s+1);
	n = strlen(s+1);
	
	for(int i = 1; i <= n; i++)		rk[i] = s[i], ++bjt[rk[i]];
	for(int i = 1; i <= 127; i++)	bjt[i] += bjt[i-1];
	for(int i = 1; i <= n; i++)		sa[bjt[rk[i]]--] = i;
	for(int i = 1; i <= n; i++)		lrk[i] = rk[i];
	for(int i = 1; i <= n; i++)
		rk[sa[i]] = rk[sa[i-1]]+(lrk[sa[i]] == lrk[sa[i-1]] ? 0 : 1);

	for(int i = 1; i < n; i <<= 1){
		//对第二关键字进行排序
		int p = 0;
		for(int j = 0; j < i; j++)	lsa[++p] = n-j;
		for(int j = 1; j <= n; j++)
			if(sa[j] > i)			lsa[++p] = sa[j]-i;
		//对第一关键字进行排序
		for(int j = 0; j <= n; j++)	bjt[j] = 0;
		for(int j = 1; j <= n; j++)	++bjt[rk[j]];
		for(int j = 1; j <= n; j++)	bjt[j] += bjt[j-1];
		for(int j = n; j >= 1; j--)	sa[bjt[rk[lsa[j]]]--] = lsa[j];
		//更新 rk 
		for(int j = 1; j <= n; j++)	lrk[j] = rk[j];
		bool flag = true;
		for(int j = 1; j <= n; j++){
			bool tmp = cmp(sa[j-1], sa[j], i);
			flag &= tmp;
			rk[sa[j]] = rk[sa[j-1]]+(tmp ? 1 : 0);
		}
		if(flag)	break;//如果 rk 互不相同,停止
	}
	for(int i = 1; i <= n; i++)	printf("%d ", sa[i]);
	
	return 0;
}

Height 数组

以下内容抄自 OIwiki

  • \(lcp(i, j)\):后缀 \(i\) 和后缀 \(j\) 的最长公共前缀。

  • \(height\) 数组:\(height[i] = lcp(sa[i], sa[i-1])\),即每一名与前一名的 \(lcp\)

  • \(O(n)\)\(height\) 数组需要的一个引理:\(height[rk[i]] \ge height[rk[i-1]]-1\),即大于等于“原字符串顺序上后一个”的 height-1。

    证明:

    先考虑后缀 \(i-1\)\(sa[rk[i-1]-1]\)\(lcp\)(长度即 \(height[rk[i-1]]\))。用一个字符串 \(aA\) 来表示这个 \(lcp\)\(a\) 是一个字符,\(A\) 是一个字符串)。

    那么后缀 \(i-1\) 即可表示为 \(aAB\),后缀 \(sa[rk[i-1]-1]\) 即可表示为 \(aAC\)\(C < B\))。

    furthermore,后缀 \(i\) 即可表示为 \(AB\),后缀 \(sa[rk[i-1]-1]+1\) 即可表示为 \(AC\)

    对于 \(i\) 在后缀数组上的前一位 \(sa[rk[i]-1]\),因为它满足所有小于后缀 \(i\) 中最大的,有:\(AC \le\) 后缀 \(sa[rk[i]-1] < AB\)

    所以后缀 \(i\) 和后缀 \(rk[i]-1\)\(lcp\) 显然会包含 \(A\)。因此,\(height[rk[i]]_{\min} = |A| = height[rk[i-1]]-1\)

inline void Height(){
	for(int i = 1, j = 0; i <= n; i++){
		if(rk[i] == 1)	continue;//根据定义 
		if(j)	--j;
		while(a[i+j] == a[sa[rk[i]-1]+j])	++j;
		ht[rk[i]] = j;
	}
	return;
}

作用:

  • 任意两个后缀的 lcp:\(\min \{height[i+1 \sim j] \}\),使用 RMQ 解决。

  • 任意两子串的大小关系:

    假设需要比较的是 \(A=S[a..b]\)\(B=S[c..d]\) 的大小关系。

    • \(lcp(a, c)\ge\min(|A|, |B|)\)\(A<B\iff |A|<|B|\)

    • 否则,\(A<B\iff rk[a]< rk[c]\)

  • 不同子串的数目:相当于统计每个后缀不同前缀的个数,用总数减去重复,即 \(\frac{n(n+1)}{2} - \sum\{height[i]\}\)

posted @ 2024-02-12 00:47  David_Mercury  阅读(18)  评论(0编辑  收藏  举报