首页 > 其他分享 >后缀数组学习笔记

后缀数组学习笔记

时间:2023-07-15 17:22:19浏览次数:53  
标签:return 后缀 rank int 笔记 数组 排序

后缀数组是什么

后缀数组就是主要处理字符串后缀问题的,它的实现算法主要有两种:倍增法和 DC3,复杂度分别是 \(O(n\log n)\) 和 \(O(n)\)。这里由于 DC3 代码答辩且难以理解,我就只写了倍增法的实现。

例题引入

P3809 【模板】后缀排序

题目大意

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

前置芝士

算法

后缀数组记录的就是字符串第 \(i\) 个后缀的排名。
比如一个字符串 \(S=abaababa\) 的后缀就是:
image
如果我们暴力去提取出 \(S\) 的所有后缀在给它排序显然是不行的,因为它的时间复杂度是 \(O(n^2\log n)\) 的。

倍增法

既然单纯的暴力不行,我们就需要考虑用倍增来处理。
倍增法的主要思想是:
每次将所有后缀按照前 \(2^k\) 个字符排序,最后得出所有后缀的排序。
这样问题就转化为了:现在已知所有后缀关于前 \(2^{k−1}\) 个字符的排序,要对所有后缀排序按前 \(2^k\) 个字符排序。
那么我们就可以将前 \(2^k\) 个字符分为两部分,每一部分的长度都是 \(2^{k−1}\),并且这两部分按照字典序排序后的名次是已知的。
这里我们定义前一部分的排名为第一关键字 \(key1\),后一部分的排名为第二关键字 \(key2\)。然后每次按照关键字进行排序,更新 \(rank\) 数组,得到新的排名。
这就是倍增法对后缀数组的处理,比如当处理 \(S=aabaaaab\) 时过程如图:

注意,这里处理第一第二关键字排序时,需要先按第一关键字的大小排序,再用第二关键字来排,其思想与基数排序很像。
由于字符串的每个后缀都是不同的(至少长度不同),所以最后得到的 \(rank\) 数组里的数必定是互不相同的。同样,如果再做第 \(k\) 次倍增时,得到的 \(rank\) 数组如果已经互不相同了,那么就说明已经找到了最终的 \(rank\) 数组,可以直接退出循环了。

实现方法

要想通过倍增法求出后缀数组有两种排序方法。

快速排序 \(O(n\log ^2n)\)

用快速排序来做倍增法是很直观,有助于初学者更好地理解后缀数组-倍增法的思路,代码也很简单。

Code

#include<bits/stdc++.h>
#define int long long
#define N 1000005
#define Mod 1000000007
#define For(i,j,k) for(long long i=j;i<=k;++i)
#define FoR(i,j,k) for(long long i=j;i<k;++i)
#define FOR(i,j,k) for(long long i=j;i>=k;--i)
using namespace std;
struct Node{
	int key1,key2,i;
}d[N];
int n,rk[N],sa[N];
string s;
inline bool cmp(Node a,Node b){
	if(a.key1<b.key1)return 1;
	if(a.key1>b.key1)return 0;
	if(a.key2<b.key2)return 1;
	if(a.key2>b.key2)return 0;
	return a.i<b.i;
}
void SA(){
	For(i,1,n)rk[i]=s[i]-'0'+1;//预处理长度为1的子串
	for(int l=1;(1<<l)<n;l++){//倍增
		int len=1<<(l-1);//已处理的长度
		For(i,1,n){//处理第一第二关键字以及它所在的后缀位置
			d[i].key1=rk[i];
			d[i].i=i;
			if(i+len<=n)//如果有第二关键字就记录
			d[i].key2=rk[i+len];
			else d[i].key2=0;//没有就将优先级变为最高
		}
		sort(d+1,d+1+n,cmp);//快排
		int rank=1;
		rk[d[1].i]=rank;
		For(i,2,n){//合并第一第二关键字
			if(d[i].key1==d[i-1].key1&&d[i].key2==d[i-1].key2)
			rk[d[i].i]=rank;
			else rk[d[i].i]=++rank;
		}
		if(rank==n)break;//如果排名已经都不相同了就退出循环
	}
	For(i,1,n)sa[rk[i]]=i;
}
signed main(){
	cin>>s;
	s=' '+s;
	n=s.size()-1;
	SA();
	For(i,1,n)printf("%lld ",sa[i]);
	return 0;
}

基数排序

可以直接把上面快排的部分直接换成基数排序。

Code

#include<bits/stdc++.h>
using namespace std;
const int maxn = 1e6+5;
int sa[maxn], rank[maxn], newRank[maxn], sum[maxn], key2[maxn];
int n, m;
char  str[maxn];
bool cmp(int a, int b, int l){
	if(rank[a] != rank[b]) return false;
	if( (a+l > n and b+l <= n) or (a+l <= n and b+l > n) ) return false;
	if(a+l > n and b+l > n) return true;
	return rank[a+l] == rank[b+l];

}
int main(){
	scanf("%s", str+1);
	n = strlen(str+1);
	for(int i=1; i<=n; i++) sum[rank[i] = str[i]]++;
	m = max(n, 256);
	for(int i=1; i<=m; i++) sum[i]+=sum[i-1];
	for(int i=n; i>=1; i--) sa[sum[rank[i]]--] = i;
	for(int l=1; l<n; l<<=1){
		int k = 0;
		for(int i=n-l+1; i<=n; i++) key2[++k] = i;
		for(int i=1; i<=n; i++) if(sa[i] > l) key2[++k] = sa[i]-l;
		for(int i=1; i<=m; i++) sum[i] = 0;
		for(int i=1; i<=n; i++) sum[rank[i]]++;
		for(int i=1; i<=m; i++) sum[i]+=sum[i-1];
		for(int i=n; i>=1; i--){
			int j = key2[i];
			sa[sum[rank[j]]--] = j;
		}
		
		int rk = 1;
		newRank[sa[1]] = rk;
		for(int i=2; i<=n; i++){ 
			if(cmp(sa[i-1], sa[i], l)) newRank[sa[i]] = rk;
			else newRank[sa[i]] = ++rk;
		}
		for(int i=1; i<=n; i++) rank[i] = newRank[i];
		
		if(rk == n) break;
	}
	
	for(int i=1; i<=n; i++) printf("%d ",sa[i]);
	return 0;
}

标签:return,后缀,rank,int,笔记,数组,排序
From: https://www.cnblogs.com/OIerBoy/p/17534580.html

相关文章

  • Perl学习笔记3_条件语句循环
    1.条件语句:if(boolean_expr0){#expr0为true时执行}elsif(boolean_expr1){#expr1为true时执行}else{#没条件匹配时执行}unless(boolean_expr0){#expr0为false时执行}elsif(boolean_expr1){#expr1为true时执行}else{#没......
  • Perl学习笔记4_命令行运行perl语句
    命令行选项例子:catfile.txt|perl-ne'$a+=s/pattern//g;END{print"$a\n"}'作用:计算文件file.txt中匹配“pattern”的个数。解释:1.cat显示文件内容,通过管道将内容送给perl程序处理;如果使用perl-e''file.txt的方式,file.txt将会被修改。使用管道,可以保证原文件......
  • Perl学习笔记2_标量数组哈希
    1.概述Perl是弱类型语言,变量不需要指定类型,解释器根据上下文自动选择匹配类型.Perl有三个基本的数据类型:标量($),数组(@),哈希(%).2.标量,scalar标量变量以$标记.my$a=123;#数字my$b="123";#字符串my$c=0x1F;#16进制my$d=047;#8进制my$e......
  • 字符串算法入门笔记
    zhx:什么AC自动机,KMP算法从来不会考zhx:不推荐用string,因为麻烦读ans入一个字符串chars[MAXN];cin>>s+1;//从s[1]开始读入,操作时方便在遍历字符串时,我们要先把字符串长度存下来,因为计算字符串长度的函数strlen的时间复杂度为\(O(长度)\),如果写成for(inti=1;i<=strlen(s+......
  • 数据结构练习笔记——输出单链表倒数第k个元素
    输出单链表倒数第k个元素【问题描述】已知带头结点的非空单链表中存放着若干整数,请找出该链表中倒数第k个元素。【输入形式】第一行:单链表中元素个数m,第二行:单链表中的m个整数,第三行:k值【输出形式】倒数第k个元素的值(不存在倒数第k个元素输出"no")【样例1】输入:5132450......
  • 第六节 数组
    1.数组概念:​ 指的是一种容器,可以同来存储同种数据类型的多个值。​ 但是数组容器在存储数据的时候,需要结合隐式转换考虑。比如:​ 定义了一个int类型的数组。那么boolean。double类型的数据是不能存到这个数组中的,​ 但是byte类型,short类型,int类型的数据是可以存到这个数组......
  • 【转】Docker入门笔记04:三大核心概念
    原文:https://zhuanlan.zhihu.com/p/312142777Docker的三大核心概念镜像Image容器Container仓库RepositoryDocker大部分的操作都围绕它的三大核心概念一、Docker镜像Docker镜像类似于虚拟机镜像,可以将它理解为一个只读的用于创建容器的模板。例如,一个镜像可以包含一个基......
  • 特殊类型注入-数组与集合
    数组给Emp添加上属性privateString[]love;表示员工爱好配置<beanid="dept"class="com.study.spring6.iocxml.deptAndEmp.Dept"><propertyname="dName"value="IT"/></bean><beanid="emp"class=......
  • 【转】Docker入门笔记01:Docker容器技术的发展历程
    原文:https://zhuanlan.zhihu.com/p/304623118最近因为工作需要,要学习一些基本的Docker知识,所以整理了一些docker的入门知识,感兴趣的小白可以看看,一起学习进步。要学习一个新的东西,我的习惯一般是先了解它是什么,它是怎么来的,发展历史是怎样的,用来解决什么问题,有什么优缺点。所以......
  • Perl学习笔记1_面向对象语法
    perl面向对象没有什么特别的语法,以例子介绍如下.例子中涉及三个文件:main.pl,AllPerson.pm,Person.pm.其中:main.pl是主脚本,它要用到AllPerson.pm.AllPerson.pm是一个class,它要用到Person.pm.Person.pm是一个class,存储人员信息.main.pl#!/usr/bin/perlusestr......