后缀数组学习笔记

1. 前置知识:基数排序

1.1. 思想

现有如下序列:3,44,38,5,47,15,36,32,50,现在要用基数排序算法排序,要怎么做?

基数排序的初始状态如下:

  1. 按照个位将原序列中的数分组,放入对应的集合

  1. 将分好的数按照个位的顺序取出,得到:

  1. 将序列中的数重新按照十位分组,放入对应集合:

  1. 将每一位上的数按从下到上的顺序依次取出,就是答案

基数排序利用的是一个桶思想,属于非比较算法

在数更多或位数更多的情况下,重复此过程即可

1.2. 代码:

#include<cstdio>
#include<algorithm>
using namespace std;
int n,a[105],cnt[15],b[105];
int main()
{
	scanf("%d",&n);
	int mx=0;
	for(int i=1;i<=n;i++)
	{
		scanf("%d",&a[i]);
		mx=max(mx,a[i]);
	}
	int d=0;
	while(mx>0)
	{
		mx/=10;
		d++;
	}
	int tmp=1;
	for(int i=1;i<=d;i++)
	{
		for(int j=0;j<10;j++) cnt[j]=0;
		for(int j=1;j<=n;j++)
		{
			int k=(a[j]/tmp)%10;
			cnt[k]++;
		}
		for(int j=1;j<10;j++)
		{
			cnt[j]+=cnt[j-1];
		}
		for(int j=n;j>0;j--)
		{
			int k=(a[j]/tmp)%10;
			b[cnt[k]]=a[j];
			cnt[k]--;
		}
		for(int j=1;j<=n;j++)
		{
			a[j]=b[j];
		}
		tmp*=10;
	}
	for(int i=1;i<=n;i++)
	{
		printf("%d ",a[i]);
	}
	return 0;
}

2.基本概念

后缀:是指从某一个位置i开始直到整个串末尾的某个子串

后缀数组:用sai表示,指所欲哦后缀在排完序后,排名为i的串在原串中的位置,通俗的讲,就是sa[]=

名次数组:用ranki表示,是指所有后缀在排完序后,原字符串中第i个后缀现在的排名,即rank[]=

aabaaaab为例,它的后缀,后缀数组,名次数组如下:

字符串的大小比较

字符串比较是逐位按字典序比较,若字典序相同,则比较下一位,否则直接分出大小,例如:

b>aaaaaaa

aab<aabc

3. 倍增求后缀数组

3.1. 思想

这里倍增比较字符串的长度

第一次是比较长度为1的字符串

第二次比较的是长度为2的字符串,可以用一个窗口[l,l+1]来表示这个字符串,显然,这个字符串是由2个相邻且长度为1的字符串拼接而成的,长度为2的字符串的排名是由两个长度为1的字符串的排名x和y组成xy

第三次比较的是长度为4的字符串,可以用一个窗口[l,l+3]来表示这个字符串,显然,这个字符串是由2个相邻且长度为2的字符串拼接而成的,长度为4的字符串的排名是由两个长度为2的字符串的排名x和y组成xy

依次类推

k次比较的是长度为2k1的字符串,可以用一个窗口[l,l+2k11]来表示这个字符串,显然,这个字符串是由2个相邻且长度为2k2的字符串拼接而成的,长度为2的字符串的排名是由两个长度为2k2的字符串的排名x和y组成xy

如何通过排名来比较字符串大小?

举个例子,两个长度为4的后缀str1和str2:

str1由两个长度为2的字符串拼成,他们的排名为x1和y1,str2由两个长度为2的字符串拼成,他们的排名为x2和y2

此时比较str1和str2的大小,可以以x为第一关键字,若x1=x2,则比较y,即y为第二关键字

注意:

  1. 在比较的过程中,如果后续的字符不够,则用0来补足

  2. 2k1n时,就会得出答案

具体比较过程如图:

这里的两个关键字,就相当于数字中的十位和个位,所以排序不分可以所以基数排序,倍增的时间复杂度为O(logn),所以总时间复杂度为O(nlog2n)

3.2. 例题

P3809 【模板】后缀排序

题目背景

这是一道模板题。

题目描述

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

输入格式

一行一个长度为 n 的仅包含大小写英文字母或数字的字符串。

输出格式

一行,共 n 个整数,表示答案。

样例 #1

样例输入 #1

ababa

样例输出 #1

5 3 1 4 2

提示

1n106

3.3. 代码

#include<cstdio>
#include<algorithm>
#include<string>
#include<iostream>
#include<cstring>
using namespace std;
const int N=2e6+5;
string s;
int n,x[N],y[N],cnt[N],sa[N],m;
int main()
{
	cin>>s;
	n=s.size();
	s=" "+s;
	m=122;
	for(int i=1;i<=n;i++) cnt[x[i]=s[i]]++;
	for(int i=1;i<=m;i++) cnt[i]+=cnt[i-1];
	for(int i=n;i>0;i--) sa[cnt[x[i]]--]=i;
	for(int k=1;k<=n;k<<=1)
	{
		memset(cnt,0,sizeof(cnt));
		for(int i=1;i<=n;i++) y[i]=sa[i];
		for(int i=1;i<=n;i++) cnt[x[y[i]+k]]++;
		for(int i=1;i<=m;i++) cnt[i]+=cnt[i-1];
		for(int i=n;i>0;i--) sa[cnt[x[y[i]+k]]--]=y[i];
		memset(cnt,0,sizeof(cnt));
		for(int i=1;i<=n;i++) y[i]=sa[i];
		for(int i=1;i<=n;i++) cnt[x[y[i]]]++;
		for(int i=1;i<=m;i++) cnt[i]+=cnt[i-1];
		for(int i=n;i>0;i--) sa[cnt[x[y[i]]]--]=y[i];
		for(int i=1;i<=n;i++) y[i]=x[i];
		m=0;
		for(int i=1;i<=n;i++)
		{
			if(y[sa[i]]==y[sa[i-1]]&&y[sa[i]+k]==y[sa[i-1]+k])
			{
				x[sa[i]]=m;
			}
			else x[sa[i]]=++m;
		}
		if(m==n) break;
	}
	for(int i=1;i<=n;i++)
	{
		printf("%d ",sa[i]);
	}
	return 0;
}

3.4. 后缀数组的应用

3.4.1. height数组

height数组:定义heighti=suffix(sai1)suffix(sai)的最长公共前缀,也就是排名相邻的两个后缀的最长公共前缀,记作heighti=lcp(sai,sai1)

如果按照height1heightn的顺序计算,时间复杂度O(n2),没有用到字符串的性质

有一个性质:

height[rank[i]]height[rank[i1]]1

证明

height[rank[i1]]1时,上式显然成立(右边小于等于 0)

height[rank[i1]]>1时:

根据height定义,有lcp(sa[rank[i1]],sa[rank[i1]1])=height[rk[i1]]>1

既然后缀i1和后缀sa[rank[i1]1]有长度为height[rank[i1]]的最长公共前缀,那么不妨用aA来表示这个最长公共前缀,其中a是一个字符,A是长度为height[rank[i1]]1的字符串

那么后缀i1可以表示为aAD,后缀sa[rank[i1]1]可以表示为$aAB。B<D,B可能为空串,D非空

进一步地,后缀i可以表示为AD,存在后缀(sa[rk[i1]1]+1AB

因为后缀sa[rk[i]1]在大小关系的排名上仅比后缀sa[rk[i]]也就是后缀i,小一位,而AB < AD。所以AB后缀sa[rk[i]1]<AD,显然后缀i和后缀sa[rk[i]1]有公共前缀 A。

于是就可以得出lcp(i,sa[rk[i]1])至少是height[rk[i1]]1,也即height[rk[i]]height[rk[i1]]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;
	}
}

同时,如果要求两个排名不连续的串的最长公共前缀,则直接取这段排名区间的最小值

例如aabaaaab中,求abaaaab和aaab的最长公共前缀,如图:

注意,最后求出来的是height[]=

3.4.2. 最长重复子串(可重叠)

显然,height所求的公共前缀必然出现了两次,所以求出所有height的最大值即可

4. 例题:

4.1. [JSOI2007] 字符加密

https://gxyzoj.com/d/gxyznoi/p/101

看到字符串排序,想后缀数组,但是如果直接排序,后面的子串就会因为不完整出现排序错误

所以,可以先将子串复制两次,然后在求sa

4.2. [luogu2408] 不同子串个数

https://gxyzoj.com/d/gxyznoi/p/102

当两个子串相等时,那么着两个子串必然是两个后缀的公共前缀

所以考虑后缀数组,排序后求出height数组,此时,如果子串s是排名相邻串的最长公共前缀,则可以直接减去长度

但是如果不相邻,则必然时其他两个相邻串的非最长公共前缀,所以在前面的统计中必然不会遗漏

4.3. [ural1297] Palindrome

https://gxyzoj.com/d/gxyznoi/p/103

从暴力开始,最简单的方法显然是枚举起点和终点,然后匹配,显然会T

考虑到,在一个回文串中,显然存在一个对称点,而将原串反转后,在原串和新串中,从对称点开始的两个后缀的公共前缀就是回文串一半(向上取整)的长度

所以可以将原串和新串拼起来,中间加一个不会出现的字符即可,记长度为n

根据上面的性质,对于中心是一个字符,求lcp(s[rank[i]],s[rank[ni+1]]),对于中心不是字符求lcp(s[rank[i]],s[rank[ni]])

4.4. [Poi2000] 公共串

https://gxyzoj.com/d/gxyznoi/p/P104

注意,是求所有串的公共子串!!!

在这些串中,任选各任选一个点,那么他们后缀的公共前缀就是公共子串

所以根据上面一道题,可以讲所有子串拼起来,中间用未出现的字符隔开,然后求后缀数组和height数组

接下来,因为任意两串的公共前缀是排名区间内取min,所以名次间隔越小,值就越大

可以使用双指针,当满足每一个子串都出现过时,求min即可

4.5. [USACO06DEC] Milk Patterns G

https://gxyzoj.com/d/gxyznoi/p/P105

要求的是重复出现的子串的最长长度,根据样例,是可重叠的,而且有出现次数的限制

可以借鉴3.4.2的思路,因为次数至少为k,所以当次数为x+1的答案为t时,次数为x的答案必然大于等于t

所以,可以找排名连续的k个后缀中的公共前缀的最小值,再去所有最小值的max,显然可以用单调队列

4.6. [SDOI2008] Sandy的卡片

https://gxyzoj.com/d/gxyznoi/p/P106

因为是在一个串中的一段同时加上同一个值之后,与其他串中经过同样处理后的某一部分相同

所以考虑差分,此时,相同的串在差分后必然相等,此时,这道题就变成了[Poi2000] 公共串

注意,因为比较的是差分数组,所以要加1

4.7. [Ahoi2013] 差异

https://gxyzoj.com/d/gxyznoi/p/P109

主要在于求lcp,直接for循环显然会T

因为lcp是一段数中的最小值,所以可以转换思路,由求一段的最小值变为求又多少种区间的最小值为当前数字

显然单调栈,注意,因为存在相等,所以边界要一边小于等于一边小于

4.8. [bzoj3230] 相似子串

https://gxyzoj.com/d/gxyznoi/p/P110

思路其实不难,因为是求前后的公共前缀,所以很自然的想到可以另外建一个反串,求它的sa

所以,现在的关键在于如何求出它的起始和终止位置的排名

可以记录从排名1到n的不同的前缀数的前缀和,即:numi=numi1+nsai+1hti

所以,可以二分解决起点问题,对于终点,因为同起点的串的排名是相邻的,而长度越短的越靠前,所以直接找到终点的起点相加即可

4.9. [NOI2015] 品酒大会

因为根据相似度的定义,就是求两个串的最长公共前缀

先考虑第一个问题

依题意得,因为当然两杯“r相似”(r>1)的酒同时也是“1相似”、“2相似”、……、“(r1)相似”的,所以可以求出每个公共前缀恰好为x的数量后求前缀和

而公共前缀恰好是x,则要求这两个串的排名之间的最小height是x

转换思路,用height考虑,因为要求最小值,考虑将height按照从大到小的顺序排序,然后依次加入

当加入一个height后,必然会将两个区域连接起来,而所有在左右各选一个点所构成的区间,最小值必然是当前值

这时只需要将左右的个数乘起来即可,因为涉及区间的合并,可以采用并查集

接下来考虑第二个问题

显然是在左和右各选一个点的值相乘,但是因为存在负数,所以还要记录最小值

posted @   wangsiqi2010916  阅读(19)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律
点击右上角即可分享
微信分享提示