后缀数组(SA)

后缀数组

P3809 【模板】后缀排序

定义:

  • 对与给定字符串的所有后缀排序后得到的sa、rk数组
    sa[i]->排名为i的后缀的位置 rk[i]->位置为i的后缀的排名
    容易发现,sa与rk互为逆,如:sa[rk[i]]=i,rk[sa[i]]=i

应用

  • 应用主要体现在用后缀数组求lcp与height来维护查找子串、匹配等问题

实现

  • 使用的牢柯前辈提供的高级思路,代码极其简略,好写好调
  • 主要思想:由于注意到后缀的特殊性,即在依据以i开始的后缀的前len位排好序后,以从i+len+1开始的后缀作为第二关键字继续排序,仍保证正确性且规模缩小(len变长了)。由于对于两个后缀,若前半部分不同,则前半部分小的必定大,否则只需比较后半部分。因此考虑倍增k,一步一步合并。
    普通的倍增做法需要记录rk1,rk2作为第一、第二关键字。但牢柯考虑将第一关键字相同的集中处理完,以第二关键字排序,便省去了记录第一关键字。同时,由于没有第一关键字,因此可以很简单地写sort。但复杂度仍然没懂,感觉不太能保证,但事实就是正确的,小柯说不太好讲,知道就行了,复杂度是O(nlogn)
  • 省掉了一个计数排序,然后就要统计这一轮的第二关键字的rk来作为下一轮的第一关键字。若两个后缀i,j本身与i+k,j+k相同,即两个后缀前k位相同,那就先视这两个后缀相同,排名不变,若不同,因为sa已经排好序,那后面的rk必定比前面大1。

code:

const int N=1e6+5;
int sa[N],rk[N],rk0[N],n,k=0; //n->s.length
string s;
bool cmp(int x,int y)
{
	return rk[x+k]<rk[y+k];
}
void SA() //sa[i]->排名为i的后缀的位置  rk[i]->位置为i的后缀的排名 
{
	for(int i=1;i<=n;i++) sa[i]=i,rk[i]=s[i];
	//初始化排名,rk可以直接等于s[i]是因为第一次倍增只要要求相同字符rk相同即可
	sort(sa+1,sa+n+1,cmp);
	//处理sa 将sa以其后缀初始位置本身的大小排序,做到要求的"相同第一关键字在sa上连续"
	for(k=1;k<=n;k<<=1) //倍增k
	{
		for(int l=1,r;l<=n;l=r+1)
		{
			r=l;
			while(r<n&&rk[sa[r]]==rk[sa[r+1]]) ++r;
			//由于保证了"相同第一关键字在sa上连续",因此直接枚举出此时第一关键字相同的区间
			sort(sa+l,sa+r+1,cmp);
			//按后缀的后半段即第二关键字排序,处理出新的sa,同时使第二关键字相同的串连续 
		}
		int m=rk0[sa[1]]=1;//赋初值
		for(int i=2;i<=n;i++) 
		rk0[sa[i]]=(rk[sa[i]]==rk[sa[i-1]]&&rk[sa[i]+k]==rk[sa[i-1]+k])?m:++m;
		//若第一、第二关键字都相同,则暂时视为同一种串
		for(int i=1;i<=n;i++) rk[i]=rk0[i];
		if(m==n) return;//若所有rk均不同,则代表已经处理完毕 
	} 
}

P4051 [JSOI2007] 字符加密

  • 题意:给定一个字符串,求将其排成一圈所有的读法排序后最后一位组成的序列。len1e6
  • 事实上很好做。考虑将字符串复制在后面,然后求后缀数组。
    显然对于后缀排序后的点与按题目中的方式排序相对位置不变,因此直接统计即可。

code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=2e5+5;
int sa[N],rk[N],rk0[N],k=0;
bool cmp(int x,int y){return rk[x+k]<rk[y+k];}
void SA(string s,int n)
{
	for(int i=1;i<=n;i++) sa[i]=i,rk[i]=s[i];
	sort(sa+1,sa+n+1,cmp);
	for(k=1;k<=n;k<<=1)
	{
		for(int l=1,r;l<=n;l=r+1)
		{
			r=l;while(r<n&&rk[sa[r]]==rk[sa[r+1]]) r++;
			sort(sa+l,sa+r+1,cmp);
		}
		int m=1;rk0[sa[1]]=1;
		for(int i=2;i<=n;i++) rk0[sa[i]]=rk[sa[i]]==rk[sa[i-1]]&&rk[sa[i]+k]==rk[sa[i-1]+k]?m:++m;
		for(int i=1;i<=n;i++) rk[i]=rk0[i];
		if(m==n) continue;
	}
}
string t;
int main()
{
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	int n;string s;cin>>s;s=s+s;n=s.length();s=' '+s;
	SA(s,n);
	for(int i=1;i<=n;i++) if(sa[i]<=n/2) t+=s[sa[i]+n/2-1];
	cout<<t;
	return 0;
}

在字符串 t 中寻找特定子串 s

  • 由于 s 如果在 t 中,那么其一定是 t 的某个后缀的前缀。由于我们已经对 t 的后缀排序,因此有可二分性。
    直接二分 O(|s|) 地比较,总时间复杂度是 O(|s|log2|t|) 的。
  • 如果要求 st 中出现的次数,也很好做。
    由于出现了 s 的后缀在后缀数组上一定是连续的,因此还是去二分区间左右端点,时间复杂度不变

P2870 [USACO07DEC] Best Cow Line G

  • 题意:从字符串首尾取字符连在一起并最小化字典序
  • 考虑类似贪心的做法。如果两边字符不同,那选更小的那一个一定优。如果相同,那就一个一个向里比,比到有不同的为止。
    但显然 O(n2),考虑有没有方法优化比较的过程。由于后缀数组是对后缀排序,而这里是对串的前缀与后缀大小比较。
    因此考虑将原串及其反串连起来。但因为如果这样原串与反串连接可能导致排序混乱,因此之间加一个特殊字符后再做sa。
    这样每次对比 O(1),就可以做到总体时间复杂度 O(nlogn)(sa本身速度是阈值)。

code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+7;
int sa[N],rk[N],rk0[N],k=0;string t;
bool cmp(int x,int y){return rk[x+k]<rk[y+k];}
void SA(string s,int n)
{
	for(int i=1;i<=n;i++) sa[i]=i,rk[i]=s[i];
	sort(sa+1,sa+n+1,cmp);
	for(k=1;k<=n;k<<=1)
	{
		for(int l=1,r;l<=n;l=r+1)
		{
			r=l;while(r<n&&rk[sa[r]]==rk[sa[r+1]]) ++r;
			sort(sa+l,sa+r+1,cmp);
		}
		rk0[sa[1]]=1;int m=1;
		for(int i=2;i<=n;i++) rk0[sa[i]]=rk[sa[i]]==rk[sa[i-1]]&&rk[sa[i]+k]==rk[sa[i-1]+k]?m:++m;
		for(int i=1;i<=n;i++) rk[i]=rk0[i];
		if(m==n) return;
	}
}
int main()
{
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	int n,nn;string s;cin>>nn;n=nn;s=' ';string x;for(int i=1;i<=n;i++) cin>>x,s+=x;s+=' ';
	for(int i=n;i>=1;i--) s+=s[i];n=s.length()-1;SA(s,n);
	int l=1,r=nn+2;
	for(int i=1;i<=nn;i++) 
	{
		if(rk[l]<rk[r]) cout<<s[l],l++;else cout<<s[r],r++;
		if(!(i%80)) cout<<'\n';
	}
	return 0;
}

lcp

  • lcp(i,j) 是指两个字符串 si,sj 的最长公共前缀。这在许多题目中是解题的关键
    下面就讲述了如何快速求字符串的两个任意子串的lcp

height数组

定义

  • heighti 表示 lcp(sa[i],sa[i1]),这里括号里的数值指的是以这个数开头的字符串的后缀,后文也有这种用法。
    特别地,height1=0

计算

  • 首先有一个定理,对于 1ilens

heightrkiheightrki11

结论详见oi wiki(还是太菜了)

  • 求法就相对显然了。由于不等式的递减性,只需要 O(n) 算出来 heightrki 的值就可以总复杂度 O(n) 地求 height 数组。
    当然也可以像oi wiki上一样正着求。

code

for(int i=1,k=0;i<=n;i++)
{
  if(k) k--;
  while(s[i+k]==s[sa[rk[i]-1]+k]) k++;
  heigth[rk[i]]=k;
}

结合height数组求lcp

  • 还是有一个定理。若求以 lcp(i,j),有

lcp(i,j)=mini+1kj(heightk)

  • 证明看起来非常专业,就不搞了。还是看oi wiki上的感性理解吧
    因此在求完 height 数组后,求 lcp 就变成了一个RMQ(区间最值)问题。一般解决方法是写一个ST表解决。

P2852 [USACO06DEC] Milk Patterns G

  • 题意:给定一串数,求出现次数至少为 k 次的子串的最大长度。
  • 显然在后缀数组拍完序后,对于所有相同子串,由于其一定是某段后缀的前缀,因此这些相同的子串一定在排完序后是连续的。
    又因为要求至少出现 k 次,因此考虑在 height 数组上用单调队列求 height 上区间的最小值。
    注意,由于 height 数组是排序后相邻两个字符串的 lcp,因此实际上单调队列维护的区间长度应该是 k1(调我两天)

code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+7;
int n,kk,s[N],sa[N],rk[N],rk0[N],k=0,q[N],h[N];
bool cmp(int x,int y){return rk[x+k]<rk[y+k];}
void SA()
{
	for(int i=1;i<=n;i++) sa[i]=i,rk[i]=s[i];
	sort(sa+1,sa+n+1,cmp);
	for(k=1;k<=n;k<<=1)
	{
		for(int l=1,r;l<=n;l=r+1)
		{
			r=l;
			while(r<n&&rk[sa[r]]==rk[sa[r+1]]) ++r;
			sort(sa+l,sa+r+1,cmp);
		}
		rk0[sa[1]]=1;int m=1;
		for(int i=2;i<=n;i++) rk0[sa[i]]=rk[sa[i]]==rk[sa[i-1]]&&rk[sa[i]+k]==rk[sa[i-1]+k]?m:++m;
		for(int i=1;i<=n;i++) rk[i]=rk0[i];
		if(m==n) return;
	}
}
void geth()
{
	for(int i=1,k=0;i<=n;i++)
	{
		if(k) k--;
		while(i+k<=n&&sa[rk[i]-1]+k<=n&&s[i+k]==s[sa[rk[i]-1]+k]) k++;
		h[rk[i]]=k;
	}
}
int main()
{
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	cin>>n>>kk;for(int i=1;i<=n;i++) cin>>s[i];
	SA();geth();kk--;
	int ans=0;
	int head=1,tail=0;
	for(int i=1;i<=n;i++)
	{
		while(head<=tail&&q[head]<=i-kk) head++;
		while(head<=tail&&h[q[tail]]>h[i]) tail--;
		q[++tail]=i;
		if(i>=kk) ans=max(ans,h[q[head]]);
	}
	cout<<ans<<'\n';
	return 0;
}

P2408 不同子串个数

  • 题意:给定一个字符串,求其本质不同的子串个数。
  • 首先,总子串个数显然为 n×(n+1)2,考虑如何判掉重复的。
    思考方式与前几道题类似,由于所有子串都是后缀的某一前缀,因此相同的子串一定是所有 lcp 的所有前缀所形成的集合。
    注意,这里是所有 lcp 的前缀所形成的集合,如果有两个相同的子串,其一定
posted @   all_for_god  阅读(11)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· 实操Deepseek接入个人知识库
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库
点击右上角即可分享
微信分享提示