SAM,即Suffix Automaton,后缀自动机。

关于字符串有很多玩法,有很多算法都是围绕字符串展开的。为什么?我的理解是:相较于数字组成的序列,字母组成的序列中每个单位上元素的个数是有限的。对于有限的东西,相较于无限的东西就会具有一些奇妙的性质。最简单的,就是序列扩展成的每个节点的儿子数是有限的。所以根据这个,从字符串Hash,到KMP,再到Suffix Array,Suffix Automaton,纷纷诞生。

后缀数组在处理字符串上相当于一把好钢,他能应付在字符串的大多数问题。那么为什么还要学SAM,很简单。SAM很有用,而且很短。优雅而有用,这就是要学的原因。

如果不说没用的话就是这玩意不容易写崩,而且可以直接构造后缀数组。

具体的细节不深究,搞OI的讲实用,所以这里面值谈构造应用,以题为主。


构造

众所周知,后缀数组的编写很糟心,常数也比较大。但是SAM好就好在他可以在线构造

先放代码:

struct SAM{
	int pre[MAXN],son[MAXN][26],cnt,len,now,step[MAXN],np,nq,p,q;
	SAM(){
		memset(pre,0,sizeof(pre));
		memset(son,0,sizeof(son));
		cnt=1;len=0;now=1;
	}
	void Extend(int nxt){
		p=now;np=++cnt;
		step[np]=step[now]+1;
		now=np;
		while(p&&!son[p][nxt]){
			son[p][nxt]=np;
			p=pre[p];
		}
		if(!p)pre[np]=1;
		else{
			q=son[p][nxt];
			if(step[q]==step[p]+1)pre[np]=q;
			else{
				step[(nq=++cnt)]=step[p]+1;
				memcpy(son[nq],son[q],sizeof(son[q]));
				pre[nq]=pre[q];
				pre[np]=pre[q]=nq;
				while(p&&son[p][nxt]==q){
					son[p][nxt]=nq;
					p=pre[p];
				}
			}
		}
	}
	int Walk(int nxt){
		while(!son[now][nxt]&&pre[now]){
			now=pre[now];
			Len=step[now];
		}
		if(!son[now][nxt])return 0;
		now=son[now][nxt];Len++;
		return Len;
	}
	void Build(){
		scanf("%s",s+1);
		int len=strlen(s+1);
		up(i,1,len)Extend(s[i]-'a');
	}
}sam;

关于构造我简单介绍一下。

首先,后缀自动机的结构是DAG。其共用了一些状态节点,同时和AC自动机类似,具有失配边。同时节点数为$O(2N)$。

后缀自动机的构造以在线添加的形式体现。就是sam.Extrend(nxt)。每调用一次最多添加$2$个节点。

每个节点有son和pre,pre指向的即为失配节点,son指向的即为儿子,同时有一个性质就是pre指向的点是可接受态的节点。

可接受态节点上代表的状态就是可以接受字母状态的节点。就是可以扩展后缀。

同时每个节点还有一个状态为len,就是当前节点到root的最长距离。

沿用构造里的变量,假设当前状态为now,我们要添加一个字符s[i],先把他转化成数字nxt然后调用Extend。

对于当前节点now,赋给p。同时新建一个节点np表示新增的一个状态同时把np的len置为len[p]+1。然后p不断沿着pre向上走,沿途把不存在nxt儿子的节点的nxt儿子赋为np,如果一直到了根节点, 说明这个字母之前字符串没有出现过。把pre[np]指向root即可。

如果走到了一个节点,他的son[p][nxt]已经被占用了,不慌,分两类情况讨论:

1.如果满足len[son[p][nxt]]==len[p]+1,很简单,直接把pre[np]指向son[p][nxt]即可。

2.如果不满足,就比较麻烦了。

先把son[p][nxt]赋给q,然后新建一个节点nq,使得len[nq]=len[q]+1,

然后把q的pre和son全部赋给nq。接着把q和np的pre指向nq。然后p沿着pre向上走,把所有满足son[p][nxt]==q的置为nq。

结束。

一些小细节建议参考代码。为什么这样构造我搞懂了会补上QAQ。

一些性质

1.$max(pre_s)=min(s)-1$

2.从$root$开始的任意一条路径都代表一个字串。

3.$right(trans(s,nxt)) \subset right(trans(pre_s,nxt))$

题目

SPOJ1811 LCS

很简单,直接暴力走就OK了。

//SPOJ1811
//by Cydiater
//2016.12.19
#include <iostream>
#include <queue>
#include <map>
#include <ctime>
#include <cstring>
#include <string>
#include <iomanip>
#include <cmath>
#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <bitset>
#include <set>
using namespace std;
#define ll long long
#define up(i,j,n)		for(int i=j;i<=n;i++)
#define down(i,j,n)		for(int i=j;i>=n;i--)
#define cmax(a,b)		a=max(a,b)
#define cmin(a,b)		a=min(a,b)
const int MAXN=5e5+5;
const int oo=0x3f3f3f3f;
char s[MAXN],t[MAXN];
int len,ans=0,Len=0;
struct SAM{
	int pre[MAXN],son[MAXN][26],cnt,len,now,step[MAXN],np,nq,p,q;
	SAM(){
		memset(pre,0,sizeof(pre));
		memset(son,0,sizeof(son));
		cnt=1;len=0;now=1;
	}
	void Extend(int nxt){
		p=now;np=++cnt;
		step[np]=step[now]+1;
		now=np;
		while(p&&!son[p][nxt]){
			son[p][nxt]=np;
			p=pre[p];
		}
		if(!p)pre[np]=1;
		else{
			q=son[p][nxt];
			if(step[q]==step[p]+1)pre[np]=q;
			else{
				step[(nq=++cnt)]=step[p]+1;
				memcpy(son[nq],son[q],sizeof(son[q]));
				pre[nq]=pre[q];
				pre[np]=pre[q]=nq;
				while(p&&son[p][nxt]==q){
					son[p][nxt]=nq;
					p=pre[p];
				}
			}
		}
	}
	int Walk(int nxt){
		while(!son[now][nxt]&&pre[now]){
			now=pre[now];
			Len=step[now];
		}
		if(!son[now][nxt])return 0;
		now=son[now][nxt];Len++;
		return Len;
	}
	void Build(){
		scanf("%s",s+1);
		int len=strlen(s+1);
		up(i,1,len)Extend(s[i]-'a');
	}
}sam;
namespace solution{
	void Prepare(){
		sam.Build();
		scanf("%s",t+1);
		len=strlen(t+1);
	}
	void Slove(){
		sam.now=1;
		up(i,1,len)cmax(ans,sam.Walk(t[i]-'a'));
		cout<<ans<<endl;
	}
}
int main(){
	//freopen("input.in","r",stdin);
	using namespace solution;
	Prepare();
	Slove();
	return 0;
}

SPOJ1812 LCS II

相对来说麻烦一些。

还是随便挑了一个串建SAM,然后剩下的串在SAM上跑,对于每个串,标记在每个节点的最大值。对于所有串,取每个节点的最小值。跑完一次后,按照拓扑序更新父亲。

//SPOJ1812
//by Cydiater
//2018.12.23
#include <iostream>
#include <cstdio>
#include <cstring>
#include <string>
#include <cmath>
#include <ctime>
#include <cstdlib>
#include <iomanip>
#include <algorithm>
#include <queue>
#include <map>
#include <bitset>
#include <set>
#include <vector>
using namespace std;
#define ll long long
#define up(i,j,n)	for(int i=j;i<=n;i++)
#define down(i,j,n)	for(int i=j;i>=n;i--)
#define cmax(a,b)	a=max(a,b)
#define cmin(a,b)	a=min(a,b)
const int MAXN=2e5+5;
const int oo=0x3f3f3f3f;
inline int read(){
	char ch=getchar();int x=0,f=1;
	while(ch>'9'||ch<'0'){if(ch=='-')f=-1;ch=getchar();}
	while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
	return x*f;
}
char s[MAXN],t[MAXN];
int flag[MAXN][15],ID=0,LEN[MAXN],ans=0,ml[MAXN],mat[MAXN],Len=0;
struct SAM{
	int son[MAXN][26],pre[MAXN],step[MAXN],p,q,np,nq,cnt,now,ma[MAXN],mi[MAXN];
	SAM(){
		cnt=now=1;
		memset(son,0,sizeof(son));
		memset(pre,0,sizeof(pre));
		memset(step,0,sizeof(step));
	}
	void Extend(int nxt){
		p=now;np=++cnt;
		step[np]=step[p]+1;
		now=np;
		while(p&&!son[p][nxt]){
			son[p][nxt]=np;
			p=pre[p];
		}
		if(!p)pre[np]=1;
		else{
			q=son[p][nxt];
			if(step[q]==step[p]+1)pre[np]=q;
			else{
				nq=++cnt;step[nq]=step[p]+1;
				memcpy(son[nq],son[q],sizeof(son[q]));
				pre[nq]=pre[q];
				pre[q]=pre[np]=nq;
				while(q&&son[p][nxt]==q){
					son[p][nxt]=nq;
					p=pre[p];
				}
			}
		}
	}
	void Build(){
		scanf("%s",s+1);
		int len=strlen(s+1);
		up(i,1,len)Extend(s[i]-'a');
		up(i,1,cnt)ml[step[i]]++;
		up(i,1,len)ml[i]+=ml[i-1];
		up(i,1,cnt)mat[ml[step[i]]--]=i;
		up(i,1,cnt)mi[i]=step[i];
	}
	void Walk(int nxt){
		while(!son[now][nxt]&&now){
			now=pre[now];
			Len=step[now];
		}
		if(!now){
			now=1;Len=0;
			return;
		}else{
			Len++;now=son[now][nxt];
			cmax(ma[now],Len);
		}
	}
}sam;
namespace solution{
	void Prepare(){
		sam.Build();
	}
	void Slove(){
		while(scanf("%s",t+1)!=EOF){
			int len=strlen(t+1);sam.now=1;Len=0;
			up(i,1,len)sam.Walk(t[i]-'a');
			down(i,sam.cnt,1){
				int t=mat[i];
				cmin(sam.mi[t],sam.ma[t]);
				if(sam.ma[t]&&sam.pre[t])sam.ma[sam.pre[t]]=sam.step[sam.pre[t]];
				sam.ma[t]=0;
			}
		}
		up(i,1,sam.cnt)cmax(ans,sam.mi[i]);
		cout<<ans<<endl;
	}
}
int main(){
	//freopen("input.in","r",stdin);
	using namespace solution;
	Prepare();
	Slove();
	return 0;
}

SAM初探的更多相关文章

  1. 【算法】后缀自动机(SAM) 初探

    [自动机] 有限状态自动机的功能是识别字符串,自动机A能识别字符串S,就记为$A(S)$=true,否则$A(S)$=false. 自动机由$alpha$(字符集),$state$(状态集合),$in ...

  2. SPOJ 1811 SAM 初探

    思路: 一个串建SAM 另一个串在SAM上跑 //By SiriusRen #include <cstdio> #include <cstring> #include < ...

  3. 【算法专题】后缀自动机SAM

    后缀自动机是用于识别子串的自动机. 学习推荐:陈立杰讲稿,本文记录重点部分和感性理解(论文语言比较严格). 刷题推荐:[后缀自动机初探],题目都来自BZOJ. [Right集合] 后缀自动机真正优于后 ...

  4. 初探领域驱动设计(2)Repository在DDD中的应用

    概述 上一篇我们算是粗略的介绍了一下DDD,我们提到了实体.值类型和领域服务,也稍微讲到了DDD中的分层结构.但这只能算是一个很简单的介绍,并且我们在上篇的末尾还留下了一些问题,其中大家讨论比较多的, ...

  5. CSharpGL(8)使用3D纹理渲染体数据 (Volume Rendering) 初探

    CSharpGL(8)使用3D纹理渲染体数据 (Volume Rendering) 初探 2016-08-13 由于CSharpGL一直在更新,现在这个教程已经不适用最新的代码了.CSharpGL源码 ...

  6. 从273二手车的M站点初探js模块化编程

    前言 这几天在看273M站点时被他们的页面交互方式所吸引,他们的首页是采用三次加载+分页的方式.也就说分为大分页和小分页两种交互.大分页就是通过分页按钮来操作,小分页是通过下拉(向下滑动)时异步加载数 ...

  7. JavaScript学习(一) —— 环境搭建与JavaScript初探

    1.开发环境搭建 本系列教程的开发工具,我们采用HBuilder. 可以去网上下载最新的版本,然后解压一下就能直接用了.学习JavaScript,环境搭建是非常简单的,或者说,只要你有一个浏览器,一个 ...

  8. .NET文件并发与RabbitMQ(初探RabbitMQ)

    本文版权归博客园和作者吴双本人共同所有.欢迎转载,转载和爬虫请注明原文地址:http://www.cnblogs.com/tdws/p/5860668.html 想必MQ这两个字母对于各位前辈们和老司 ...

  9. React Native初探

    前言 很久之前就想研究React Native了,但是一直没有落地的机会,我一直认为一个技术要有落地的场景才有研究的意义,刚好最近迎来了新的APP,在可控的范围内,我们可以在上面做任何想做的事情. P ...

随机推荐

  1. 北京54全国80及WGS84坐标系的相互转换

    这三个坐标系统是当前国内较为常用的,它们均采用不同的椭球基准.其中北京54坐标系,属三心坐标系,大地原点在苏联的普而科沃,长轴6378245m,短轴6356863,扁率1/298.3:西安80坐标系, ...

  2. 高仿ios版美团框架项目源码

    高仿美团框架基本已搭好.代码简单易懂,适合新人.适合新人.新人. <ignore_js_op>     源码你可以到ios教程网那里下载吧,这里我就不上传了,http://ios.662p ...

  3. nodejs pm2部署配置

    pm2是一个进程管理工具,可以用它来管理你的node进程,并查看node进程的状态,当然也支持性能监控,进程守护,负载均衡等功能. 1.pm2安装使用需要全局安装  npm install -g pm ...

  4. Oracle常用函数汇总

    在Oracle OCP考试中,相当一部分知识点涉及到对于Oracle常见函数的考查.尽管Oracle官方文档SQL Language Reference中Functions一章内列举了所有Oracle ...

  5. PHP中的数据库一、MySQL优化策略综述

    前些天看到一篇文章说到PHP的瓶颈很多情况下不在PHP自身,而在于数据库.我们都知道,PHP开发中,数据的增删改查是核心.为了提升PHP的运行效率,程序员不光需要写出逻辑清晰,效率很高的代码,还要能对 ...

  6. how2heap分析系列:0

    新学期到了,给学弟们写点东西, https://github.com/shellphish/how2heap 这个how2heap挺不错的,讲述了heap上几种不同的漏洞利用技术,在后面发的几篇中我会 ...

  7. WPF 自定义列表筛选 自定义TreeView模板 自定义ListBox模板

    有很多项目,都有数据筛选的操作.下面提供一个案例,给大家做参考. 左侧是数据源,搜索框加TreeView控件,右侧是ListBox控件.在左侧数据列点击添加数据,然后点击确定,得到所筛选的数据. 下面 ...

  8. Linux 信号(一)—— kill 函数

    世事并无好坏之分,全看我们怎么去想.—— 哈姆雷特·第二幕第二景 ilocker:关注 Android 安全(新入行,0基础) QQ: 2597294287 #include <signal.h ...

  9. [游戏开发-学习笔记]菜鸟慢慢飞(九)- NGUI- UIWidget(官方说明翻译)

  10. js小技巧

    js判断字符长度 直接使用String对象的属性,空格亦算一个字符 myString = "Hello world"; length = myString.length js比较字 ...