后缀数组

后缀数组主要是两个数组:\(sa\)\(rk\)

其中,\(sa[i]\)表示将所有后缀排序后第\(i\)小的后缀的编号,\(rk[i]\)表示后缀\(i\)的排名

这两个数组满足性质:\(sa[rk[i]] = ra[sa[i]] = i\)

后缀数组示例:

后缀数组的求法

\(O(n^2logn)\)做法:

这个做法大家都会,暴力嘛,用\(string + sort\)就可以了

\(O(nlog^2n)\)做法:

这个做法用到了倍增的思想

先对每个字符(长度为1的子串)进行排序

假设我们已经知道了长度为\(w\)的子串的排名\(rk_w[1…n]\)(即,\(rk_w[i]\)表示\(s[i…\min(i + w - 1,n)]\)\(\{s[x…\min(x +w - 1,n)|x\in[1,n]\}\)中的排名),那么以\(rk_w[i]\)为第一关键字,\(rk_w[i+w]\)为第二关键字(若\(i+w>n\)则令\(rk_w[i+w]\)为无穷小)进行排序,就可以求出\(rk_2w[1…n]\)

倍增排序示意图:

如果用\(sort\)进行排序,复杂度就是\(O(nlog^2n)\)

#define B cout << "BreakPoint" << endl;
#define O(x) cout << #x << " " << x << endl;
#define O_(x) cout << #x << " " << x << " ";
#define Msz(x) cout << "Sizeof " << #x << " " << sizeof(x)/1024/1024 << " MB" << endl;
#include<cstdio>
#include<cmath>
#include<iostream>
#include<cstring>
#include<algorithm>
#include<queue>
#include<set>
#define LL long long
const int inf = 1e9 + 9;
const int N = 2e5 + 5;
using namespace std;
inline int read() {
	int s = 0,w = 1;
	char ch = getchar();
	while(ch < '0' || ch > '9') {
		if(ch == '-')
			w = -1;
		ch = getchar();
	}
	while(ch >= '0' && ch <= '9') {
		s = s * 10 + ch - '0';
		ch = getchar();
	}
	return s * w;
}
char s[N];
int n,w,p,sa[N],rk[N << 1],oldrk[N << 1];
// 为了防止访问 rk[i+w] 导致数组越界,开两倍数组。
// 当然也可以在访问前判断是否越界,但直接开两倍数组方便一些。
int main() {
	scanf("%s", s + 1);
  	n = strlen(s + 1);
  	for(int i = 1; i <= n; i++) rk[i] = s[i];
  	for(w = 1; w < n; w <<= 1) {
    	for(int i = 1; i <= n; i++) sa[i] = i;
    	sort(sa + 1, sa + n + 1,[](int x, int y) {return rk[x] == rk[y] ? rk[x + w] < rk[y + w] : rk[x] < rk[y];});
    	memcpy(oldrk, rk, sizeof(rk));
    	// 由于计算 rk 的时候原来的 rk 会被覆盖,要先复制一份
    	for(int i = 1;i <= n;i++) {
    		p = 0;
      		if(oldrk[sa[i]] == oldrk[sa[i - 1]] && oldrk[sa[i] + w] == oldrk[sa[i - 1] + w])  rk[sa[i]] = p; 
	  		else rk[sa[i]] = ++p;  // 若两个子串相同,它们对应的 rk 也需要相同,所以要去重
    	}
  	}
  	for(int i = 1; i <= n; i++) printf("%d ", sa[i]);
  	return 0;
}

\(O(nlogn)\)做法

\(O(nlog^2n)\)做法中,单次排序是\(O(nlogn)\)的,如果能\(O(n)\)排序即可

前置芝士:基数排序,计数排序

然而我也不会这个做法。

就这地吧

后缀数组

寻找最小的循环移动位置

将字符串\(s\)复制一份变成\(ss\)就转化成了后缀排序问题

例题: JSOI2007」字符加密

在字符串中找子串

任务 在线地在主串\(T\)中寻找模式串\(S\)。在线的意思是,我们已经预先知道主串\(T\),但是当且晋档询问时才知道模式串\(S\)。我们可以先构造出\(T\)的后缀数组,然后查找子串\(S\)。若子串\(S\)\(T\)中出现,它必定是\(T\)的一些后缀的前缀。因为我们已将所有后缀排序了,我们额可以在通过\(p\)数组中二分\(S\)来实现。比较子串\(S\)和当前后缀的时间复杂度为\(O(|S|)\),因此找子串的时间复杂度为\(O(|s|log|T|)\)。注意,如果该子串在\(T\)中国出现了多次,每次出现都是在\(p\)数组中相邻的。因此出现次数可以通过再次二分找到,输出每次出现的位置也很轻松。

从字符串首位取字符最小化字典序

例题: 「USACO07DEC」Best Cow Line

题意:给你一个字符串,每次从首或尾取一个字符组成字符串,问所有能够组成的字符串中字典序最小的一个。

暴力做法就是每次最坏\(O(n)\)地判断当前应该取首还是尾(即比较取首得到的字符串与取尾得到的反串的大小),只需优化这一判断过程即可。

由于需要在原串后缀与反串后缀构成的集合内比较大小,可以将反串拼接在原串后,并在中间加上一个没出现过的字符(如 #,代码中可以直接使用空字符),求后缀数组,即可\(O(1)\)完成这一判断。

height数组

LCP(最长公共前缀)

两个字符串\(S\)\(T\)的LCP就是最大的\(x\)\(x \leq \min(|S|,|T|)\))使得\(S_i = T_i(\forall1\leq i\leq x)\)

\(lcp(i,j)\)表示后缀\(i\)和后缀\(j\)的最长公共前缀(的长度)

height数组的定义

\(height[i] = lcp(sa[i],sa[i-1])\),即第\(i\)名的后缀与它前一名的后缀的最长公共前缀。

\(height[1]\)可以视作0

\(O(n)\)\(height\)数组需要一个引理

\(height[rk[i]] \ge height[rk[i - 1]] - 1\)

证明:

\(height[rk[i-1]]\leq 1\)时,上式显然成立(右边小于等于0)

\(height[rk[i - 1]]>1\)时:

设后缀\(i-1\)\(aAD\)\(A\)是一个长度为\(height[rk[i-1]]-1\)的字符串),那么后缀\(i\)就是\(AD\)。设后缀\(sa[rk[i-1]-1]\)\(aAB\),那么\(lcp(i - 1,sa[rk[i-1]-1])=aA\)。由于后缀\(sa[rk[i-1]-1]+1\)\(AB\),一定飘在后缀\(i\)的前面,所后缀数组\(sa[rk[i]-1]\)一定含有前缀\(A\),所以\(lcp(i,sa[rk[i]-1])\)至少是\(height[rk[i-1]]-1\)

\[i-1:AD\\i:AD\\sa[rk[i-1]-1]:aAb\\sa[rk[i]-1]:A[B/C]\\lcp(i,sa[rk[i]-1]):AX(X可能为空) \]

\(O(n)\)求height数组的代码实现

利用上面这个引理暴力求:

for(int i = 1, k = 0; i <= n; ++i) {
  if (k) --k;
  while(s[i + k] == s[sa[rk[i] - 1] + k]) ++k;
  ht[rk[i]] = k;  // height太长了缩写为ht
}

\(k\)不会超过\(n\),最多减\(n\)次,所以最多加\(2n\)次,总复杂度就是\(O(n)\)

height数组的应用

两子串最长公共前缀

\(lcp(sa[i],sa[j]) = \min\{ height[i+1…j]\}\)

感性理解:如果\(height\)一直大于某个数,前这么多为一直就没变过;反之,由于后缀已经排好序了,不可能变了之后变回来。

有了这个定理,求两子串最长公共前缀就转化为了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[b]\)

不同子串的数目

子串就是后缀的前缀,所以可以每个后缀,计算前缀总数,再减掉重复。

“前缀总数”其实就是子串个数,为\(n(n+1)/2\)

如果按后缀排序的顺序枚举后缀,每次新增的子串就是除了与上一个后缀的LCP剩下的前缀。这些前缀一定是新增的,否则会破坏\(lcp(sa[i],s[j])=\min\{ height[i+1..j]\}\)的性质。只有这些前缀是新增的,因为LCP部分在枚举上一个前缀时计算过了

所以答案为

\[\frac{n(n+1)}{2}-\sum_{i=2}^nheight[i] \]

posted @ 2020-02-23 22:38  优秀的渣渣禹  阅读(307)  评论(0编辑  收藏  举报