📚【模板】后缀数组



后缀数组

对于一个字符串 \(pattern\),我们想求它的每一个后缀的排名 \(rank_i\),以及排名为 \(k\) 的后缀是哪一个 \(suffix_k\)

这就是后缀数组的事了。


一般路过倍增方法( \(O(n\log^2 n)\) )

通过一张简明的图片,我们有了一种方法

不断地倍增 \(w\),每次以 \(rank_i\) 为第一关键字,\(rank_{i+w}\) 为第二关键字排序。

倍增需要 \(O(\log n)\) 次,每次排序需要 \(O(n \log n)\),时间复杂度 \(O(n \log^2 n)\)

#include <stdio.h>
#include <string.h>
#include <bits/stl_algobase.h>
#include <bits/stl_algo.h>
const int N = 1048576;
char temp[N];
int rank[N<<1], sa[N];
int copy[N<<1];
int n, i;
void sa_get() {
	n = strlen(temp+1);
	for(int i = 1;i <= n;++i) {
		sa[i] = i;
		rank[i] = temp[i];
	}
	for(i = 1;i < n;i <<= 1) {
		std :: sort(sa+1,sa+n+1,[](int _a,int _b) {
			return rank[_a]^rank[_b] ? 
					rank[_a] < rank[_b] : 
					rank[_a+i] < rank[_b+i];
		});
		memcpy(copy,rank,(n+1)*sizeof(int));
		for(int j = 0, k = 1;k <= n;++k) {
			if(copy[sa[k]] == copy[sa[k-1]]&&copy[sa[k]+i] == copy[sa[k-1]+i]) 
				rank[sa[k]] = j;
			else 
				rank[sa[k]] = ++j;
		}
	}
}
signed main() {
	scanf("%s",temp+1);
	sa_get();
	for(int i = 1;i <= n;++i) 
		printf("%d ",sa[i]);
}

基于计数排序的优化( \(O(n \log n)\) )

上面那份板子显然看起来不是很好

虽然过了板子题,但是单数据用时达到了令人谔谔的 \(600\,ms\)\(5e8\) 以内的质数都比这快

好了,咱们来点优化:

  1. 对于两个关键字使用基数排序和计数排序;

  2. 考虑到先把第二关键字顺序确定,再对第一关键字跑计数排序(计数排序是稳定排序);

  3. 每次更新一下值域 \(m\),不要每次对着 \(\operatorname{length}(pattern)\) 的值域跑计数排序;

  4. 减少内存不连续访问。

const int N = 1e6+10;
int rank[N], suffix[N];
int cnt[N], id[N], key_1[N], copy[N<<1];
int n, m = 127;
bool comp(int x,int y,int w) {
	return copy[x] == copy[y]&&copy[x+w] == copy[y+w];
}
void get_sa(char *pattern) {
	int p, i;
	for(i = 1;i <= n;++i) 
		++cnt[rank[i] = pattern[i]];
	for(i = 1;i <= m;++i) 
		cnt[i] += cnt[i-1];
	for(i = n;i >= 1;--i) 
		suffix[cnt[rank[i]]--] = i;
	for(int w = 1;;w <<= 1, m = p) {
		for(p = 0, i = n;i > n-w;--i) 
			id[++p] = i;
		for(i = 1;i <= n;++i) 
			if(suffix[i] > w) 
				id[++p] = suffix[i]-w;
		//只要我们把最后的 w 个先放进数组,再按 suffix 数组的顺序放
		//那么第二关键字就是有序的了,而计数排序是稳定排序,不会破坏第二关键字的顺序
		memset(cnt,0,(m+1)*sizeof(int));
		for(i = 1;i <= n;++i) 
			++cnt[key_1[i] = rank[id[i]]];
		for(i = 1;i <= m;++i) 
			cnt[i] += cnt[i-1];
		for(i = n;i >= 1;--i) 
			suffix[cnt[key_1[i]]--] = id[i];
		//计数排序过程
		memcpy(copy+1,rank+1,n*sizeof(int));
		for(p = 0, i = 1;i <= n;++i) 
			rank[suffix[i]] = comp(suffix[i],suffix[i-1],w) ? p : ++p;
		//计算新的 rank 数组
		if(p == n) {
			for(i = 1;i <= n;++i) 
				suffix[rank[i]] = i;
			break;
		}
	}
}

这样就可以快乐地切掉板子题了!

\(\textrm{luogu P3809 【模板】后缀排序}\)

\(\textrm{Loj #111. 后缀排序}\)

现在时间开销就少多了:


后缀数组的一些应用



height 数组

\(\operatorname{lcp}(S,T)\):表示字符串 \(S,T\) 的最长公共前缀。

下文记 \(\operatorname{lcp}(i,j)\) 为字符串 \(pattern\) 的后缀 \(i\) 与后缀 \(j\) 的最长公共前缀。

那么 \(height_i\) 定义为字符串 \(pattern\)\(i\) 名后缀与其前一名后缀的最长公共前缀,记为 \(height_i = \operatorname{lcp}(suffix_i,suffix_{i-1})\)


利用 \(rank\) 数组方便地求 \(height\) 数组( \(O(n)\) )

有一个引理:

\[height_{rank_i} \ge height_{rank_{i-1}}-1 \]

证明
int height[N];
int get_height(char *pattern) {
	for(int i = 1, k = 0;i <= n;++i) {
		if(rank[i] == 0) 
			continue;
		if(k) 
			--k;
		while(pattern[i+k] == pattern[suffix[rank[i]-1]+k]) 
			++k;
		height[rank[i]] = k;
	}
}


posted @ 2022-10-09 08:38  bikuhiku  阅读(36)  评论(2编辑  收藏  举报