首页 > 其他分享 >后缀数组

后缀数组

时间:2023-08-06 20:28:31浏览次数:62  
标签:LCP 后缀 height int 数组 sa rk

SA

基数排序

一般采用 LSD(Least Significant Digital),从键值的最低位开始排序。

定义

记 \(suf(i)\) 为起始下标为 \(i\) 的后缀。

记 \(sa[i]\) 为排名第 \(i\) 的后缀的起始位置。

记 \(rk[i]\) 为 \(suf(i)\) 的排名。


P3809 【模板】后缀排序

对于一个长为 \(n\) 的字符串,求出其后缀数组 \(\{sa_n\}\).

倍增法

  • 把每个字符当作长为 \(1\) 的后缀,按字符值进行编号。

  • 将当前长度为 \(len\) 的后缀扩展至 \(2len\),按前半、后半部分的编号进行排序。

  • 时间复杂度 \(O(n\log n)\),空间线性。

具体地,第二步中的前、后两部分会形成若干个二元组。

直接排序的复杂度为 \(O(n\log^2 n)\).

使用 LSD 进行基数排序,以第二维为第一关键字。

我们可以拿一个 \(tp\) 记录第二关键字中排名为 \(i\) 的数的位置。

#include<bits/stdc++.h>
#define N 1000010
using namespace std;
int n,m;char s[N];
int a[N],sa[N],rk[N],tp[N];
int get(char c){
	if(c>='0'&&c<='9')return c-'0'+1;
	if(c>='A'&&c<='Z')return c-'A'+11;
	return c-'a'+37;
}
void init(){
	n=strlen(s+1),m=62;
	for(int i=1;i<=n;i++)
		rk[i]=get(s[i]),tp[i]=i;
}
void LSD(){//基数排序
	for(int i=1;i<=m;i++)a[i]=0;
	for(int i=1;i<=n;i++)a[rk[i]]++;
	for(int i=1;i<=m;i++)a[i]+=a[i-1];
	for(int i=n;i;i--)sa[a[rk[tp[i]]]--]=tp[i];
}
void SA(){
	for(int w=1,p=0;w<=n;m=p,p=0,w<<=1){
		for(int i=n-w+1;i<=n;i++)tp[++p]=i;
    		//p记录最高排名,若p=n则无需排序
            	//n-w+1后面均无第二关键字,补0设成极小值排在前面
		for(int i=1;i<=n;i++)
			if(sa[i]>w)tp[++p]=sa[i]-w;
		LSD(),swap(rk,tp),p=rk[sa[1]]=1;
        	//有rk[sa[i]]=i
		for(int i=2;i<=n;i++)
			rk[sa[i]]=(tp[sa[i]]==tp[sa[i-1]]&&
				tp[sa[i]+w]==tp[sa[i-1]+w])?p:++p;
                //两后缀相等排名不变
		if(p==n)return;
	}
}
int main(){
	scanf("%s",s+1);
	init(),LSD(),SA();
	for(int i=1;i<=n;i++)
		printf("%d ",sa[i]);
	printf("\n");
	
	return 0;
}

感性理解一下吧。时间复杂度 \(O(n\log n)\)
.

  • 不要使用任何 vector.

一些应用

  • 最小循环移动位置

这个概念比较奇怪但是可以看看 P4051 [JSOI2007] 字符加密

令 \(S\leftarrow SS\),求一遍 SA 即可。

本题字符集包含了字符所以可以让程序里的 \(m\) 取一个大值。

main 函数:

int main(){
	scanf("%s",_s+1),_n=strlen(_s+1);
	for(int i=1;i<=_n;i++)
		s[i]=s[i+_n]=_s[i];
	init(),LSD(),SA();
	for(int i=1;i<=n;i++){
		if(sa[i]<=_n)
			printf("%c",s[sa[i]+_n-1]);
	}
	printf("\n");
	
	return 0;
}
  • 匹配子串

在线地在主串 \(T\) 里寻找模式串 \(S\).

做出 \(T\) 的 SA,若 \(S\) 在 \(T\) 中出现,那么 \(S\) 是 \(T\) 的某个后缀的前缀。

在排序过的后缀里二分,一次比较的时间为 \(O(|S|)\),故总时间 \(O(|S|\log|T|)\).

若求出现次数,则其在 SA 数组中相邻,同样上一次二分即可。

  • 取首尾最小化字典序

P2870 [USACO07DEC] Best Cow Line G

暴力做法即单次 \(O(n)\) 比较当前串与反串的大小。考虑优化。

一个十分方便的想法是将原串与反串拼接,以 # 隔开,这样就能够做到 \(O(n\log n)\) 预处理,单次 \(O(1)\) 的复杂度。

main 函数:

int main(){
	scanf("%d",&_n),getchar();
	for(int i=1;i<=_n;i++){
		scanf("%c",&s[i]),getchar();
		s[_n*2+1-i+1]=s[i];
	}
	s[_n+1]='#';
	init(),LSD(),SA();
	for(int i=1,l=1,r=_n;i<=_n;i++){
		printf("%c",rk[l]<rk[n+1-r]?s[l++]:s[r--]);
		if(i%80==0)printf("\n");
	}
	
	return 0;
}

height 数组

  • LCP(最长公共前缀)

这里我们记 \(lcp(i,j)\) 为后缀 \(i\) 和 \(j\) 的 LCP(的长度)。

  • height 数组的定义

\(height[i]=lcp(sa[i],sa[i-1])\),即第 \(i\) 名的后缀与 \(i-1\) 名的后缀的 LCP。

\(height[1]\) 视作 \(0\).

  • height 数组求解

引理:\(height[rk[i]]\ge height[rk[i-1]]-1\)

证明:当 \(height[rk[i-1]]\le 1\) 时显然成立。下面对 \(height[rk[i-1]]>1\) 的情况进行讨论。

由 height 数组的定义有 \(lcp(sa[rk[i-1]],sa[rk[i-1]-1])=height[rk[i-1]]>1\).

也就是后缀 \(i-1\) 和后缀 \(sa[rk[i-1]-1]\) 有长为 \(height[rk[i-1]]\) 的 LCP。用一个字符 \(a\) 和一个串 \(A\) 表示它。

即 \(LCP=aA\),且 \(|A|=height[rk[i-1]]-1\).

后缀 \(i-1\) 可表示为 \(aAD\),后缀 \(sa[rk[i-1]-1]\) 可表示为 \(aAB\),且有 \(B<D\),\(D\) 非空,\(B\) 可空。

后缀 \(i\) 可表示为 \(AD\),存在后缀 \((sa[rk[i-1]-1]+1)\) 为 \(AB\).

由于后缀 \(sa[rk[i]-1]\) 在大小排名上仅比后缀 \(sa[rk[i]]=i\) 小一位,\(AB<AD\),所以有 \(AB\le\text{后缀}sa[rk[i]-1]<AD\).

显然后缀 \(i\) 和后缀 \(sa[rk[i]-1]\) 有公共前缀 \(A\).

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

实在看不懂就感性理解了。

线性复杂度代码实现

根据引力暴力求解。

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

\(k\) 恒小于等于 \(n\),最多减 \(n\) 次,加 \(2n\) 次。

故复杂度线性。


一些应用

  • 两子串 LCP

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

若 \(height\) 一直大于某个数,则前这么多位一直不变。反之,由于后缀排过序,变化的位不可能变回来。

由此两子串的 LCP 转化成了一个 RMQ 问题。

  • 比较两子串大小

记两者为 \(A=s[a\sim b]\),\(B=s[c\sim d]\).

若 \(lcp(a,c)\ge min(|A|,|B|)\),\(A<B\Longleftrightarrow |A|<|B|\).

否则,\(A<B\Longleftrightarrow rk[a]<rk[c]\).

  • 不同字串数目

P2408 不同子串个数

子串即后缀的前缀,枚举每个后缀,计算前缀总数再减掉重复的。

前缀总数即 \(\displaystyle\frac{n(n+1)}{2}\).

将后缀排序后枚举,新增的子串是除了与上一个后缀的 LCP 剩下的前缀。

答案是

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

record

  • 出现次数至少为 \(k\) 的最长子串

P2852 [USACO06DEC] Milk Patterns G

取所有相邻的 \(k-1\) 个 \(height\) 的 \(\min\) 的 \(\max\).

配合单调队列实现。

--

标签:LCP,后缀,height,int,数组,sa,rk
From: https://www.cnblogs.com/SError0819/p/17609906.html

相关文章

  • C语言定义并初始化一个二维数组(利用指针数组)
    C语言定义并初始化一个二维数组(利用指针数组),可以实现二位数组的每一行的元素个数不同1.代码如下#include<stdio.h>#include<stdlib.h>intmain(){//arr是一个指针数组,即这个数组的所有元素都是指针,每一个元素都指向一个int型数组,每一个int型数组的长度可以不同......
  • 后缀自动机
    定义字符串\(s\)的SAM是一个接受\(s\)的所有后缀的最小DFA(确定性有限(状态)自动机)。也就是:SAM是一个DAG。节点为状态,边为转移。图的源点\(t_0\)称初始状态。整张图从\(t_0\)开始可以遍历到。转移标有若干字母,从一个节点出发的所有转移均不同。存在若干终......
  • 2023-08-06:小青蛙住在一条河边, 它想到河对岸的学校去学习 小青蛙打算经过河里 的石头
    2023-08-06:小青蛙住在一条河边,它想到河对岸的学校去学习小青蛙打算经过河里的石头跳到对岸河里的石头排成了一条直线,小青蛙每次跳跃必须落在一块石头或者岸上给定一个长度为n的数组arr,表示每块儿石头的高度数值每块石头有一个高度,每次小青蛙从一块石头起跳这块石头的高度就......
  • 2023-08-06:小青蛙住在一条河边, 它想到河对岸的学校去学习 小青蛙打算经过河里 的石头
    2023-08-06:小青蛙住在一条河边,它想到河对岸的学校去学习小青蛙打算经过河里的石头跳到对岸河里的石头排成了一条直线,小青蛙每次跳跃必须落在一块石头或者岸上给定一个长度为n的数组arr,表示每块儿石头的高度数值每块石头有一个高度,每次小青蛙从一块石头起跳这块石头的......
  • 数组,条件,循环,重要函数,超级全局变量,魔术方法
    目录数组,条件,循环,实战重要函数超级全局变量魔术方法数组,条件,循环,实战数组在PHP中,array()函数用于创建数组:$cars=array("Volvo","BMW","Toyota");在PHP中,有三种类型的数组:数值数组-带有数字ID键的数组关联数组-带有指定的键的数组,每个键关联一个值......
  • 【JavaScript03】Array数组对象基本操作
    首先定义一个数组,可以用[];也可以使用newArray()来创建一个数组对象数组通过下标取值数组通过下标取值,从0开始在python中可以通过下标-1反着取倒数第一个值,JavaScript中没这种取值方法.当数组的下标不在它取值范围内,如x有4个成员,那么取值是0-3,非0-3的数字下标取值,得到......
  • 【动态规划】【力扣357次周赛】6953. 判断是否能拆分数组
    【力扣357次周赛】6953.判断是否能拆分数组给你一个长度为n的数组nums和一个整数m。请你判断能否执行一系列操作,将数组拆分成n个非空数组。在每一步操作中,你可以选择一个长度至少为2的现有数组(之前步骤的结果)并将其拆分成2个子数组,而得到的每个子数组,至少需......
  • ES6数组reduce方法使用
    reduce方法对数组中的每个元素执行一个reducer函数,将其减少为单个值。reduce的语法如下:letresult=arr.reduce(reducer,initialValue);reducer函数包含四个参数:accumulator-累计器,默认为initialValue的值,累计回调函数的返回值currentValue-数组中正在处理的元素......
  • C关于一维数组以及二维数组的创建和简单利用(上)
    第一段代码#include<stdio.h>#include<String.h>intmain(){inta[]={1,2,3,4,5,6,7,8,9,10};intb=sizeof(a)/sizeof(a[0]);intc=0;for(c=0;c<b;c+=1){printf("&a[%d]=%p\n",c,&a[c]);}......
  • 王道408用数组,链表以及双向链表实现栈、队列
    我在电脑上敲了一遍,又在纸上模拟了一遍下面记录在电脑上敲的:一、用数组实现栈#include<stdio.h>#include<string.h>#defineMaxSize50typedefstruct{intdata[MaxSize];inttop;}stack;voidInitStack(stack&S){S.top=-1;S.data[0]=5;......