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

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\)

void get_height(){
  for(int i=1;i<=n;i++) rk[sa[i]]=i;
  for(int i=1,k=0;i<=n;i++){
  	if(rk[i]==1) continue;
  	if(k) k--;
  	int j=sa[rk[i]-1];
  	while(i+k<=n&&j+k<=n&&s[i+k]==s[j+k]) k++;
  	height[rk[i]]=k;
  }
}

SP1811 LCS - Longest Common Substring

题目描述

传送门
求两个串的最长公共子串长度,每个串的长度不超过 250000

Solution

考虑把这两个字符串接起来
注意中间要用一个奇怪的字符隔开
然后再整体跑后缀数组和LCP
这样对于每一个后缀,
我们去找离它最近的其它串的后缀
再求LCP最后ans取max即可

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

const int maxn=5e5+10;
int n,m,rk[maxn],p,sa[maxn],c[maxn],y[maxn],h[maxn],lst[2],ans=0,mn[2];
char s[maxn];

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(n==num) return;
  }
}

void getheight(){
  for(int i=1;i<=n;i++) rk[sa[i]]=i;
  for(int i=1,k=0;i<=n;i++){
  	if(rk[i]==1) continue;
  	if(k) k--;
  	int j=sa[rk[i]-1];
  	while(i+k<=n&&j+k<=n&&s[i+k]==s[j+k]) k++;
  	h[rk[i]]=k;
  }
}

void getans(){
  lst[0]=lst[1]=0;mn[0]=mn[1]=0x3f3f3f3f;
  for(int i=1;i<=n;i++){
  	mn[0]=min(mn[0],h[i]);
  	mn[1]=min(mn[1],h[i]);
  	if(sa[i]<=p){
  	  lst[0]=i;mn[0]=0x3f3f3f3f;
  	  if(!lst[1]) continue;
  	  ans=max(ans,mn[1]);
	}
	else if(sa[i]>p+1){
	  lst[1]=i;mn[1]=0x3f3f3f3f;
	  if(!lst[0]) continue;
	  ans=max(ans,mn[0]);
	}
  }
}

int main(){
  /*2023.7.26 H_W_Y SP1811 LCS - Longest Common Substring 后缀数组*/ 
  scanf("%s",s+1);p=strlen(s+1);s[p+1]='$';
  scanf("%s",s+p+2);n=strlen(s+1);
  getsa();getheight();getans();
  printf("%d\n",ans);
  return 0;
}

P4051 [JSOI2007] 字符加密-寻找最小的循环移动位置

题目描述

传送门
简单易懂。

Solution

我们考虑把原字符串 \(S\) 复制一份变成 \(SS\)
这样直接进行后缀排序即可
一道板子题……

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

const int N=2e5+5;
int n,sa[N],c[N],cnt=0,rk[N],y[N],m,len;
char s[N];

void getsa(){
  m=200;
  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];
  	num=rk[sa[1]]=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.29 H_W_Y P4051 [JSOI2007] 字符加密 后缀数组*/ 
  scanf("%s",s+1);
  n=strlen(s+1);len=n;n*=2;
  for(int i=len+1;i<=n;i++) s[i]=s[i-len];
  getsa();
  for(int i=1;i<=n;i++)
    if(sa[i]<=len) putchar(s[sa[i]+len-1]);
  printf("\n");
  return 0;
}

P2870 [USACO07DEC] Best Cow Line G-从字符串首尾取字符最小化字典序

题目描述

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

Solution

发现如果暴力去枚举
我们必须要找到两端往里面走第一个不同的位置才行
而最坏情况是 AAAAAAAAAAA
这样进行暴力时间复杂度就是 \(O(n^2)\)

发现每一次其实我们都是去找的后缀
从后往前也就是把序列翻转过来的后缀
这样就想到了后缀数组

我们把这个序列和翻转的序列放在一起,中间加上一个奇怪的字符
这样一起跑后缀数组
在后面枚举时直接 \(O(1)\) 比较即可
也可以用哈希但是我不会

注意 \(c\) 数组的大小也是 \(N\)

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

const int N=2e6+5;
int n,rk[N],sa[N],c[N],y[N],len=0,l,r,m,cnt=0;
char s[N];

void getsa(){
  m=200;
  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;
  }
  for(int i=1;i<=n;i++) rk[sa[i]]=i;
}

int main(){
  /*2023.7.29 H_W_Y P2870 [USACO07DEC] Best Cow Line G 后缀数组*/ 
  scanf("%d",&n);
  for(int i=1;i<=n;i++){
  	s[i]=getchar();
  	while(!(s[i]>='A'&&s[i]<='Z')) s[i]=getchar();
  	s[2*n+2-i]=s[i];
  }
  s[n+1]='$';len=n;n=2*n+1;
  getsa();l=1,r=len;
  while(l<=r){
  	printf("%c",rk[l]<rk[n+1-r]?s[l++]:s[r--]);
  	if((++cnt)%80==0) puts("");
  }
  return 0;
}

P2852 [USACO06DEC] Milk Patterns G-出现至少 k 次的子串的最大长度

题目描述

传送门
出现至少 k 次的子串的最大长度

Solution

思路非常简单
就直接求一个 height 就可以了
把连续 \(k-1\)\(h[i]\) 取 min 答案再去 max
注意 \(h[i]\) 的下标 \(i\) 是排名
函数不要写错了……

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

const int N=1e5+10;
int c[N<<5],n,m,a[N],rk[N],sa[N],y[N],ans=0,h[N],q[N],head,tail,p;

void getsa(){
  m=1e6;
  for(int i=1;i<=n;i++) rk[i]=a[i],c[a[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];
  	num=rk[sa[1]]=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) return;  
  }
}

void getheight(){
  for(int i=1;i<=n;i++) rk[sa[i]]=i;
  for(int i=1,k=0;i<=n;i++){
  	if(rk[i]==1) continue;
  	if(k) k--;
  	int j=sa[rk[i]-1];
    while(i+k<=n&&j+k<=n&&a[i+k]==a[j+k]) k++;
    h[rk[i]]=k;
  }
}

int main(){
  /*2023.7.29 H_W_Y P2852 [USACO06DEC] Milk Patterns G 后缀数组*/ 
  scanf("%d%d",&n,&p);
  for(int i=1;i<=n;i++) scanf("%d",&a[i]);
  getsa();getheight(); 
  head=1;tail=0;
  for(int i=2;i<=p;i++){
  	while(head<=tail&&h[i]<h[q[tail]]) tail--;
  	q[++tail]=i;
  }
  ans=h[q[head]];
  for(int i=p+1;i<=n;i++){
  	while(head<=tail&&q[head]<=i-p+1) head++;
  	while(head<=tail&&h[i]<h[q[tail]]) tail--;
  	q[++tail]=i;
  	ans=max(ans,h[q[head]]);
  }
  printf("%d\n",ans);
  return 0;
}
posted @ 2023-07-26 16:17  H_W_Y  阅读(29)  评论(0编辑  收藏  举报