20230726-后缀数组SA+后缀自动机SAM

发布时间 2023-07-26 16:24:44作者: H_W_Y

20230726

后缀数组

后缀数组 (SA, Suffix Array)
是将字符串的所有后缀排序得到的数组,主要包括两个数组:
\(sa[i]\):将所有后缀按字典序排序后\(i\) 小的后缀的开头位置。
\(rk[i]\):表示从第 \(i\) 个字符开始的后缀(我们将它称为后缀 \(i\))的字典序排名
它们满足 \(sa[rk[i]] = rk[sa[i]] = i\)

image
后缀数组能够帮助我们快速解决许多关于子串、后缀的字符串题目
这里介绍后缀数组的 \(O(n log n)\) 倍增求法。

正常比较字符串的字典序需要先比较第一位,再比较第二位 . . . 。
倍增法,顾名思义就是依次比较所有后缀的前 \(k = 1, 2, 4, . . . 2^c\) 位。

首先按照每个后缀的第一个字符对后缀进行排序
这相当于将这个字符串的每个字符进行排序。

接下来考虑已经对 \(k = n\) 排好了序,如何对 \(k = 2n\) 排序:
将每个子串拆分为前 \(k\) 位与第 \(k + 1 ∼ 2k\) 位,如果能分别求出这两部分的排序,
那么对 \(k = 2n\) 排序相当于对这样一个二元组进行双关键字排序。
\(k\) 位的排序已知,对于第 \(k + 1 ∼ 2k\) 位,
注意到后缀 \(i\) 的第 \(k + 1 ∼ 2k\) 位,
就是后缀 \(i + k\) 的前 \(k\) 位,
因此这部分的排序可以通过前 \(k\) 位的排序整体移 \(k\)\(O(n)\) 得到。
最后的双关键字排序由于值域只有 \(n\),可以使用基数排序做到 \(O(n)\)
这样就可以 \(O(n)\) 得到 \(k = 2n\) 的排序
总复杂度 \(O(n log n)\)

代码还是很优美的

P3809 【模板】后缀排序

题目描述

传送门
读入一个长度为 \(n\) 的由大小写英文字母或数字组成的字符串,
请把这个字符串的所有非空后缀按字典序(用 ASCII 数值比较)从小到大排序,
然后按顺序输出后缀的第一个字符在原串中的位置。
位置编号为 \(1\)\(n\)
用后缀 \(i\) 表示第 \(i\) 位开始的后缀。
\(n \le 10^6\)

Solution

模板题

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

const int N=1e6+10;
int n,m,rk[N],sa[N],y[N],c[N];
char s[N];

void getsa(){
  m=122;
  for(int i=1;i<=n;i++) rk[i]=s[i],c[rk[i]]++;
  for(int i=1;i<=m;i++) c[i]+=c[i-1];
  for(int i=1;i<=n;i++) sa[c[rk[i]]--]=i;
  for(int k=1;;k<<=1){
  	int num=0;
  	for(int i=n-k+1;i<=n;i++) y[++num]=i;
  	for(int i=1;i<=n;i++) if(sa[i]>k) y[++num]=sa[i]-k;
	for(int i=1;i<=m;i++) c[i]=0;
	for(int i=1;i<=n;i++) c[rk[i]]++;
	for(int i=1;i<=m;i++) c[i]+=c[i-1];
	for(int i=n;i>=1;i--) sa[c[rk[y[i]]]--]=y[i],y[i]=rk[i];
	rk[sa[1]]=num=1;
	for(int i=2;i<=n;i++){
	  if(y[sa[i]]==y[sa[i-1]]&&y[sa[i]+k]==y[sa[i-1]+k]) rk[sa[i]]=num;
	  else rk[sa[i]]=++num;
	}m=num;
	if(num==n) break;
  }
}

int main(){
  /*2023.7.26 H_W_Y P3809 【模板】后缀排序 后缀数组*/ 
  scanf("%s",s+1);n=strlen(s+1);
  getsa();
  for(int i=1;i<=n;i++) printf("%d ",sa[i]);
  return 0;
}

最长公共前缀-LCP

定义 \(LCP(i, j)\) 表示后缀 \(sa[i]\) 与后缀 \(sa[j]\) 的最长公共前缀。
首先可以只用考虑 \(i \lt j\) 的情况:

  • \(LCP(i, j) = LCP(j, i)\)
  • \(LCP(i, i) = n − sa[i] + 1\)

LCP Lemma

\(LCP(i, j) = min(LCP(i, k), LCP(k, j))(1 \le i \le k \le j \le n)\)
证明
\(t = min(LCP(i, k), LCP(k, j))\)
那么 \(LCP(i, k) \ge t, LCP(k, j) \ge t\)
后缀 \(sa[i]\)\(t\)\(= sa[k]\)\(t\)\(= sa[j]\) 的前 \(t\) 位,
\(LCP(i, j) \ge t\)
\(LCP(i, j) = q \gt t\)
那么 \(i, j\) 的前 \(q\) 个字符相等,
因为 \(t = min(LCP(i, k), LCP(k, j))\)
所以要么 \(sa[i][t + 1]\)(表示后缀 \(sa[i]\) 的第 \(t + 1\)位)\(\lt sa[k][t + 1]\)
要么 \(sa[k][t + 1] \lt sa[j][t + 1]\)
并且 \(sa[i][t + 1] \le sa[k][t + 1] \le sa[j][t + 1]\)
所以 \(sa[i][t + 1] \ne sa[j][t + 1]\)
与假设矛盾,所以 \(LCP(i, j) = t\)

LCP Theorem

\(LCP(i, j) = min(LCP(k − 1, k)) , k \in (i, j]\)
证明
由 LCP Lemma:
\(LCP(i, j) = min(LCP(i, i + 1), LCP(i + 1, j)\),
然后继续拆下去即可证明。

height数组

\(height[i] = LCP(i, i − 1), height[1] = 0\)
那么求出 height
LCP 就是个区间 min 了
\(h[i] = height[rk[i]]\)
于是 \(height[i] = h[sa[i]]\)
\(h[i]\),有定理:
\(h[i] \ge h[i − 1] − 1\)

证明:
首先我们假设 \(sa[rk[i] − 1] = j, sa[rk[i − 1] − 1] = k\)
于是 \(h[i] = LCP(j, i), h[i − 1] = LCP(k, i − 1)\)
于是我们只需证明 \(LCP(j, i) \ge LCP(k, i − 1) − 1\)
如果后缀 \(k\) 与后缀 \(i − 1\) 首字母不同,显然成立。
如果后缀 \(k\) 与后缀 \(i − 1\) 首字母相同,
那么分别去掉首字母后得到后缀 \(k + 1\) 与后缀 \(i\)
必有 \(rk[k + 1]\)\(\lt rk[i]\)
于是 \(LCP(k + 1, i) = h[i − 1] − 1\)
对于字符串 \(i\)
所有排名比它靠前的字符串中,
与它相似度最高也就是 LCP 最大的一定是紧挨着它的字符串,即 \(j\)
但我们已知 \(k + 1\) 排在 \(i\) 前面并且
\(LCP(k + 1, i) = h[i − 1] − 1\)
那么必然有
\(LCP(j, i) \ge LCP(k + 1, i) = h[i − 1] − 1\)
\(h[i] \ge h[i − 1] + 1\)