当前位置: 首页 > news >正文

【后缀自动机】hihocoder1441 后缀自动机一·基本概念

 

时间限制:10000ms
单点时限:1000ms
内存限制:256MB

描述

小Hi:今天我们来学习一个强大的字符串处理工具:后缀自动机(Suffix Automaton,简称SAM)。对于一个字符串S,它对应的后缀自动机是一个最小的确定有限状态自动机(DFA),接受且只接受S的后缀。

小Hi:比如对于字符串S="aabbabd",它的后缀自动机是:

其中红色状态是终结状态。你可以发现对于S的后缀,我们都可以从S出发沿着字符标示的路径(蓝色实线)转移,最终到达终结状态。例如"bd"对应的路径是S59,"abd"对应的路径是S189,"abbabd"对应的路径是S184679。而对于不是S后缀的字符串,你会发现从S出发,最后会到达非终结状态或者“无路可走”。特别的,对于S的子串,最终会到达一个合法状态。例如"abba"路径是S1846,"bbab"路径是S5467。而对于其他不是S子串的字符串,最终会“无路可走”。 例如"aba"对应S18X,"aaba"对应S123X。(X表示没有转移匹配该字符)

小Ho:好像很厉害的样子!对于任意字符串都能构造出一个SAM吗?另外图中那些绿色虚线是什么?

小Hi:是的,任意字符串都能构造出一个SAM。我们知道SAM本质上是一个DFA,DFA可以用一个五元组 <字符集,状态集,转移函数、起始状态、终结状态集>来表示。下面我们将依次介绍对于一个给定的字符串S如何确定它对应的 状态集 和 转移函数 。至于那些绿色虚线虽然不是DFA的一部分,却是SAM的重要部分,有了这些链接SAM是如虎添翼,我们后面再细讲。

SAM的States

小Hi:这一节我们将介绍给定一个字符串S,如何确定S对应的SAM有哪些状态。首先我们先介绍一个概念 子串的结束位置集合 endpos。对于S的一个子串s,endpos(s) = s在S中所有出现的结束位置集合。还是以S="aabbabd"为例,endpos("ab") = {3, 6},因为"ab"一共出现了2次,结束位置分别是3和6。同理endpos("a") = {1, 2, 5}, endpos("abba") = {5}。

小Hi:我们把S的所有子串的endpos都求出来。如果两个子串的endpos相等,就把这两个子串归为一类。最终这些endpos的等价类就构成的SAM的状态集合。例如对于S="aabbabd":

状态子串endpos
S空串{0,1,2,3,4,5,6}
1a{1,2,5}
2aa{2}
3aab{3}
4aabb,abb,bb{4}
5b{3,4,6}
6aabba,abba,bba,ba{5}
7aabbab,abbab,bbab,bab{6}
8ab{3,6}
9aabbabd,abbabd,bbabd,babd,abd,bd,d{7}

小Ho:这些状态恰好就是上面SAM图中的状态。

小Hi:没错。此外,这些状态还有一些美妙的性质,且等我一一道来。首先对于S的两个子串s1和s2,不妨设length(s1) <= length(s2),那么 s1是s2的后缀当且仅当endpos(s1) ⊇ endpos(s2),s1不是s2的后缀当且仅当endpos(s1) ∩ endpos(s2) = ∅。

小Ho:我验证一下啊... 比如"ab"是"aabbab"的后缀,而endpos("ab")={3,6},endpos("aabbab")={6},是成立的。"b"是"ab"的后缀,endpos("b")={3,4,6}, endpos("ab")={3,6}也是成立的。"ab"不是"abb"的后缀,endpos("ab")={3,6},endpos("abb")={4},两者没有交集也是成立的。怎么证明呢?

小Hi:证明还是比较直观的。首先证明s1是s2的后缀=>endpos(s1) ⊇ endpos(s2):既然s1是s2后缀,所以每次s2出现时s1以必然伴随出现,所以有endpos(s1) ⊇ endpos(s2)。再证明endpos(s1) ⊇ endpos(s2)=>s1是s2的后缀:我们知道对于S的子串s2,endpos(s2)不会是空集,所以endpos(s1) ⊇ endpos(s2)=>存在结束位置x使得s1结束于x,并且s2也结束于x,又length(s1) <= length(s2),所以s1是s2的后缀。综上我们可知s1是s2的后缀当且仅当endpos(s1) ⊇ endpos(s2)。s1不是s2的后缀当且仅当endpos(s1) ∩ endpos(s2) = ∅是一个简单的推论,不再赘述。

小Ho:我好像对SAM的状态有一些认识了!我刚才看上面的表格就觉得SAM的一个状态里包含的子串好像有规律。考虑到SAM中的一个状态包含的子串都具有相同的endpos,那它们应该都互为后缀?

小Hi:你观察力还挺敏锐的。下面我们就来讲讲一个状态包含的子串究竟有什么关系。上文提到我们把S的所有子串按endpos分类,每一类就代表一个状态,所以我们可以认为一个状态包含了若干个子串。我们用substrings(st)表示状态st中包含的所有子串的集合,longest(st)表示st包含的最长的子串,shortest(st)表示st包含的最短的子串。例如对于状态7,substring(7)={aabbab,abbab,bbab,bab},longest(7)=aabbab,shortest(7)=bab。

小Hi:对于一个状态st,以及任意s∈substrings(st),都有s是longest(st)的后缀。证明比较容易,因为endpos(s)=endpos(longest(st)),所以endpos(s) ⊇ endpos(longest(st)),根据我们刚才证明的结论有s是longest(st)的后缀。

小Hi:此外,对于一个状态st,以及任意的longest(st)的后缀s,如果s的长度满足:length(shortest(st)) <= length(s) <= length(longsest(st)),那么s∈substrings(st)。 证明也是比较容易,因为:length(shortest(st)) <= length(s) <= length(longsest(st)),所以endpos(shortest(st)) ⊇ endpos(s) ⊇ endpos(longest(st)), 又endpos(shortest(st)) = endpos(longest(st)),所以endpos(shortest(st)) = endpos(s) = endpos(longest(st)),所以s∈substrings(st)。

小Ho:这么说来,substrings(st)包含的是longest(st)的一系列连续后缀?

小Hi:没错。比如你看状态7中包含的就是aabbab的长度分别是6,5,4,3的后缀;状态6包含的是aabba的长度分别是5,4,3,2的后缀。

SAM的Suffix Links

小Hi:前面我们讲到substrings(st)包含的是longest(st)的一系列连续后缀。这连续的后缀在某个地方会“断掉”。比如状态7,包含的子串依次是aabbab,abbab,bbab,bab。按照连续的规律下一个子串应该是"ab",但是"ab"没在状态7里,你能想到这是为什么么?

小Ho:aabbab,abbab,bbab,bab的endpos都是{6},下一个"ab"当然也在结束位置6出现过,但是"ab"还在结束位置3出现过,所以"ab"比aabbab,abbab,bbab,bab出现次数更多,于是就被分配到一个新的状态中了。

小Hi:没错,当longest(st)的某个后缀s在新的位置出现时,就会“断掉”,s会属于新的状态。比如上例中"ab"就属于状态8,endpos("ab"}={3,6}。当我们进一步考虑"ab"的下一个后缀"b"时,也会遇到相同的情况:"b"还在新的位置4出现过,所以endpos("b")={3,4,6},b属于状态5。在接下去处理"b"的后缀我们会遇到空串,endpos("")={0,1,2,3,4,5,6},状态是起始状态S。

小Hi:于是我们可以发现一条状态序列:7->8->5->S。这个序列的意义是longest(7)即aabbab的后缀依次在状态7、8、5、S中。我们用Suffix Link这一串状态链接起来,这条link就是上图中的绿色虚线。

小Ho:原来如此。

小Hi:Suffix Links后面会有妙用,我们暂且按下不表。

SAM的Transition Function

小Hi:最后我们来介绍SAM的转移函数。对于一个状态st,我们首先找到从它开始下一个遇到的字符可能是哪些。我们将st遇到的下一个字符集合记作next(st),有next(st) = {S[i+1] | i ∈ endpos(st)}。例如next(S)={S[1], S[2], S[3], S[4], S[5], S[6], S[7]}={a, b, d},next(8)={S[4], S[7]}={b, d}。

小Hi:对于一个状态st来说和一个next(st)中的字符c,你会发现substrings(st)中的所有子串后面接上一个字符c之后,新的子串仍然都属于同一个状态。比如对于状态4,next(4)={a},aabb,abb,bb后面接上字符a得到aabba,abba,bba,这些子串都属于状态6。

小Hi:所以我们对于一个状态st和一个字符c∈next(st),可以定义转移函数trans(st, c) = x | longest(st) + c ∈ substrings(x) 。换句话说,我们在longest(st)(随便哪个子串都会得到相同的结果)后面接上一个字符c得到一个新的子串s,找到包含s的状态x,那么trans(st, c)就等于x。

小Ho:吼~ 终于把SAM中各个部分搞明白了。

小Hi:SAM的构造有时空复杂度均为O(length(S))的算法,我们将在后面介绍。这一期你可以先用暴力算法依照定义构造SAM,先对SAM有个直观认识再说。

小Ho:没问题,暴力算法我最拿手了。我先写程序去了。

输入

第一行包含一个字符串S,S长度不超过50。

第二行包含一个整数N,表示询问的数目。(1 <= N <= 10)

以下N行每行包括一个S的子串s,s不为空串。

输出

对于每一个询问s,求出包含s的状态st,输出一行依次包含shortest(st)、longest(st)和endpos(st)。其中endpos(st)由小到大输出,之间用一个空格分割。

样例输入
aabbabd  
5  
b  
abbab  
aa  
aabbab  
bb  
样例输出
b b 3 4 6  
bab aabbab 6  
aa aa 2  
bab aabbab 6  
bb aabb 4

SAM存个板子!hihocoder讲得真的很详细!通俗易懂!endpos集合从slink_tree的子节点递推出来,由于字符串长度很小,直接用long long存储该集合即可。

#include<cstdio>
#include<cstring>
using namespace std;
#define MAXL 50
#define MAXC 26
typedef long long ll;
int v[2*MAXL+10],__next[2*MAXL+10],first[2*MAXL+10],e;
void AddEdge(int U,int V){
	v[++e]=V;
	__next[e]=first[U];
	first[U]=e;
}
char s[MAXL+10];//文本串
int len/*文本串长度*/;
struct SAM{
	ll endposes[2*MAXL+10];
	int endcnt[2*MAXL+10];
	int n/*状态数0~n-1*/,maxlen[2*MAXL+10],minlen[2*MAXL+10],trans[2*MAXL+10][MAXC],slink[2*MAXL+10];
	int new_state(int _maxlen,int _minlen,int _trans[],int _slink){
		maxlen[n]=_maxlen;
		minlen[n]=_minlen;
		for(int i=0;i<MAXC;++i){
			if(_trans==NULL){
				trans[n][i]=-1;
			}
			else{
				trans[n][i]=_trans[i];
			}    
		}
		slink[n]=_slink;
		return n++;
	}
	int add_char(char ch,int u,int pos){
		if(u==-1){
			return new_state(0,0,NULL,-1);
		}
		int c=ch-'a';
		int z=new_state(maxlen[u]+1,-1,NULL,-1);
		endposes[z]=(1ll<<pos);
		endcnt[z]=1;
		int v=u;
		while(v!=-1 && trans[v][c]==-1){
			trans[v][c]=z;
			v=slink[v];
		}
		if(v==-1){//最简单的情况,suffix-path(u->S)上都没有对应字符ch的转移
			minlen[z]=1;
			slink[z]=0;
			return z;
		}
		int x=trans[v][c];
		if(maxlen[v]+1==maxlen[x]){//较简单的情况,不用拆分x
			minlen[z]=maxlen[x]+1;
			slink[z]=x;
			return z;
		}
		int y=new_state(maxlen[v]+1,-1,trans[x],slink[x]);//最复杂的情况,拆分x
		slink[y]=slink[x];
		minlen[x]=maxlen[y]+1;
		slink[x]=y;
		minlen[z]=maxlen[y]+1;
		slink[z]=y;
		int w=v;
		while(w!=-1 && trans[w][c]==x){
			trans[w][c]=y;
			w=slink[w];
		}
		minlen[y]=maxlen[slink[y]]+1;
		return z;
	}
	void dfs(int U){
		for(int i=first[U];i;i=__next[i]){
			dfs(v[i]);
			endposes[U]|=endposes[v[i]];
			endcnt[U]+=endcnt[v[i]];
		}
	}
	void work_slink_tree(){
		for(int i=1;i<n;++i){
			AddEdge(slink[i],i);
		}
		dfs(0);
	}
}sam;
int m;
int main(){
//	freopen("hihocoder1441.in","r",stdin);
//	freopen("hihocoder1441.out","w",stdout);
	char T[MAXL+10];
	scanf("%s",s);
	len=strlen(s);
	int U=sam.add_char(0,-1,0);
	for(int i=0;i<len;++i){
		U=sam.add_char(s[i],U,i);
	}
	sam.work_slink_tree();
	scanf("%d",&m);
	for(int i=1;i<=m;++i){
		scanf("%s",T);
		int lenT=strlen(T);
		U=0;
		for(int j=0;j<lenT;++j){
			U=sam.trans[U][T[j]-'a'];
		}
		int k=0;
		for(int j=0;j<len;++j){
			if((sam.endposes[U]>>j)&1){
				++k;
				if(k==1){
					for(int l=j-sam.minlen[U]+1;l<=j;++l){
						putchar(s[l]);
					}
					putchar(' ');
					for(int l=j-sam.maxlen[U]+1;l<=j;++l){
						putchar(s[l]);
					}
					putchar(' ');
				}
				printf("%d%c",j+1,k==sam.endcnt[U] ? '\n' : ' ');
			}
		}
	}
	return 0;
}

转载于:https://www.cnblogs.com/autsky-jadek/p/6636972.html

相关文章:

  • kafka之zk搭建
  • 机器学习 —— 基础整理(一)贝叶斯决策论;二次判别函数;贝叶斯错误率;生成式模型的参数方法...
  • C# Int转Enum
  • 开启TDE的RDS SQL Server还原到本地环境
  • 如何使用thinkphp的model来验证前端表单?
  • 磁盘文件系统2
  • 201521123009 《Java程序设计》第6周学习总结
  • Java深入 - 深入理解Java集合
  • [转] CSSOM视图模式(CSSOM View Module)相关整理
  • 批处理数字前加0
  • MySQL innodb_table_stats表不存在的解决方法
  • 鬼谷子的局 有感
  • bash脚本总结1:[[:not found 错误
  • Maven(一)-- 基础知识
  • Atitit 数据库视图与表的wrap与层级查询规范
  • 《Java编程思想》读书笔记-对象导论
  • 10个确保微服务与容器安全的最佳实践
  • CSS中外联样式表代表的含义
  • DOM的那些事
  • IDEA常用插件整理
  • Javascripit类型转换比较那点事儿,双等号(==)
  • Java新版本的开发已正式进入轨道,版本号18.3
  • js面向对象
  • LeetCode算法系列_0891_子序列宽度之和
  • Map集合、散列表、红黑树介绍
  • Redux 中间件分析
  • windows-nginx-https-本地配置
  • 基于Android乐音识别(2)
  • 模型微调
  • 前嗅ForeSpider采集配置界面介绍
  • 小程序开发之路(一)
  • 携程小程序初体验
  • 一、python与pycharm的安装
  • # C++之functional库用法整理
  • # Python csv、xlsx、json、二进制(MP3) 文件读写基本使用
  • ### Cause: com.mysql.jdbc.exceptions.jdbc4.MySQLTr
  • ### Error querying database. Cause: com.mysql.jdbc.exceptions.jdbc4.CommunicationsException
  • (2021|NIPS,扩散,无条件分数估计,条件分数估计)无分类器引导扩散
  • (9)STL算法之逆转旋转
  • (附源码)ssm高校运动会管理系统 毕业设计 020419
  • (免费领源码)Java#ssm#MySQL 创意商城03663-计算机毕业设计项目选题推荐
  • (一)Neo4j下载安装以及初次使用
  • (原)本想说脏话,奈何已放下
  • ../depcomp: line 571: exec: g++: not found
  • .NET Core 实现 Redis 批量查询指定格式的Key
  • .Net Remoting(分离服务程序实现) - Part.3
  • .net websocket 获取http登录的用户_如何解密浏览器的登录密码?获取浏览器内用户信息?...
  • .net 桌面开发 运行一阵子就自动关闭_聊城旋转门家用价格大约是多少,全自动旋转门,期待合作...
  • .NET正则基础之——正则委托
  • .Net转Java自学之路—基础巩固篇十三(集合)
  • @基于大模型的旅游路线推荐方案
  • [AIGC] Kong:一个强大的 API 网关和服务平台
  • [Codeforces] probabilities (R1600) Part.1
  • [C语言]——柔性数组
  • [FTP]pureftp部署和优化