<学习笔记> SAM
SAM
定义#
字符串
-
子串 在原串 中所有出现位置(最后一个字符位置)的集合。 -
这个节点所代表的等价类中最大子串长。 -
后缀链接。 -
由后缀链接构成的树。
性质#
引理一:字符串
的两个非空子串 和 (假设 )的 相同,当且仅当字符串 在 中的每次出现,都是以 后缀的形式存在的。
引理二: 考虑两个非空子串
和 (假设 )。那么要么 ,要么 ,取决于 是否为 的一个后缀:
引理三: 考虑一个
等价类,将类中的所有子串按长度非递增的顺序排序。每个子串都不会比它前一个子串长,与此同时每个子串也是它前一个子串的后缀。换句话说,对于同一等价类的任一两子串,较短者为较长者的后缀,且该等价类中的子串长度恰好覆盖整个区间 。
引理四: 所有后缀链接构成一棵根节点为
的树。
-
根据引理三可知,
,所以后缀树上这个点到根的并为 。 -
又根据引理二,后缀树上
的祖先满足均为 的后缀,所以两个前缀的最长公共后缀为这两个点在后缀树上 的长度。根据这个就可以求 。 -
状态数
;转移数 。 -
后缀树的叶子节点为原串的一个前缀,每个节点的
集合为其子树中前缀的个数。
构建#
现在,任务转化为实现给当前字符串添加一个字符
-
令
为添加字符 之前,整个字符串对应的状态(一开始我们设 ,算法的最后一步更新 )。 -
创建一个新的状态
,并将 赋值为 ,在这时 的值还未知。 -
现在我们按以下流程进行(从状态
开始)。如果还没有到字符 的转移,我们就添加一个到状态 的转移(相当于那些所代表的后缀都可以加一个字符变成一个新的后缀),遍历后缀链接。如果在某个点已经存在到字符 的转移,我们就停下来,并将这个状态标记为 。 -
如果没有找到这样的状态
,我们就到达了虚拟状态 ,我们将 赋值为 并退出。 -
假设现在我们找到了一个状态
,其可以通过字符 转移。我们将转移到的状态标记为 。 -
如果
,我们只要将 赋值为 并退出。(这样其实就是 这个节点所代表的状态均可以作为 的后缀,可以证明一下)。
设
后缀路径上的前一个状态为 。根据操作,可知 有一条转移边。则此时 ,说明 恰好与 的后缀链接的定义相匹配。
- 否则就会有些复杂。需要复制状态
:我们创建一个新的状态 ,复制 的除了 的值以外的所有信息(后缀链接和转移)。我们将 赋值为 。
复制之后,我们将后缀链接从 指向 ,也从 指向 。
最终我们需要使用后缀链接从状态 往回走,只要存在一条通过 到状态 的转移,就将该转移重定向到状态 。
, ,
。
新建点的原因是因为加入
这个字符,使得 的 集合变成两类,一类比另一类多一个 。所以要新建一个节点将这个区分出来,所以一开始将 的状态大多数赋给 ,因为是由一个点拆出来的,需要将 的所有存在 的转移边的全部改为指向 ,这样就可以将 合法。
code
void insert(int c){
int cur=++tot;
len[cur]=len[last]+1;
int p=last;
while(p!=-1 && !ch[p][c]){
ch[p][c]=cur;
p=_link[p];
}
if(p==-1) _link[cur]=0;
else{
int q=ch[p][c];
if(len[q]==len[p]+1) _link[cur]=q;
else{
int clone=++tot;
len[clone]=len[p]+1,_link[clone]=_link[q];
for(int i=0;i<26;i++) ch[clone][i]=ch[q][i];
while(p!=-1 && ch[p][c]==q){
ch[p][c]=clone;
p=_link[p];
}
_link[cur]=_link[q]=clone;
}
}
last=cur;
}
应用#
两个字符串的最长公共子串#
对一个串建
code
int v=0,l=0;
for(int i=1;i<=m;i++){
int c=b[i]-'a';
if(ch[v][c]){
v=ch[v][c];
l++;
}
else{
int p=v;
while(p!=-1 && !ch[p][c]) p=_link[p];
if(p==-1) v=l=0;
else{
v=ch[p][c];
l=len[p]+1;
}
}
ans=max(ans,l);
}
不同子串个数#
方法一:其实就是求从
答案为
方法二:
如果求子串可以重复,然后你要求解
那么就变成了 ,相当于有那么多的位置可以选。
字典序第 k 大子串#
其实就是从
例题#
差异#
两个前缀的公共后缀就是
code
#include<bits/stdc++.h>
using namespace std;
const int N=2*5*1e5+5;
int ch[N][26],len[N],_link[N],tot,last;
char s[N];
void init(){
len[0]=0,_link[0]=-1;
tot=last=0;
}
int siz[N];
void insert(int c){
int cur=++tot;
len[cur]=len[last]+1;
int p=last;
siz[tot]=1;
while(p!=-1 && !ch[p][c]){
ch[p][c]=cur;
p=_link[p];
}
if(p==-1) _link[cur]=0;
else{
int q=ch[p][c];
if(len[p]+1==len[q]) _link[cur]=q;
else{
int clone=++tot;
len[clone]=len[p]+1,_link[clone]=_link[q];
for(int i=0;i<26;i++) ch[clone][i]=ch[q][i];
while(p!=-1 && ch[p][c]==q){
ch[p][c]=clone;
p=_link[p];
}
_link[cur]=_link[q]=clone;
}
}
last=cur;
}
int head[N*2],nex[N*2],ver[N*2],idx=0;
void add(int x,int y){
ver[++idx]=y,nex[idx]=head[x],head[x]=idx;
}
long long ans=0;
void dfs(int x){
for(int i=head[x];i;i=nex[i]){
int y=ver[i];
dfs(y);
ans-=2ll*len[x]*siz[x]*siz[y];
siz[x]+=siz[y];
}
}
signed main(){
scanf("%s",s+1);
int n=strlen(s+1);
init();
for(int i=n;i>=1;i--) insert(s[i]-'a');
for(int i=1;i<=tot;i++) add(_link[i],i);
for(int i=1;i<=n;i++) ans+=1ll*i*(n-1);
dfs(0);
printf("%lld",ans);
}
熟悉的文章#
首先这个有单调性,所以可以二分。然后对于判断可以进行一个
然后如何找最长匹配后缀。一位一位考虑,在
code
#include<bits/stdc++.h>
using namespace std;
const int N=2*1e6+10;
char a[N];
int tr[N][2],fa[N],str[N],idx=0;
int ch[N][2],len[N],_link[N],last[N],tot=0;
void init(){
_link[0]=-1,tot=0;len[0]=0;
}
queue<int> q;
void ins(char *s){
int n=strlen(s+1);
int u=0;
for(int i=1;i<=n;i++){
int c=s[i]-'0';
if(!tr[u][c]) tr[u][c]=++idx;
fa[tr[u][c]]=u,str[tr[u][c]]=c;
u=tr[u][c];
}
}
int insert(int las,int c){
int cur=++tot;
len[cur]=len[las]+1;
int p=las;
while(p!=-1 && !ch[p][c]){
ch[p][c]=cur;
p=_link[p];
}
if(p==-1) _link[cur]=0;
else{
int q=ch[p][c];
if(len[q]==len[p]+1) _link[cur]=q;
else{
int clone=++tot;
len[clone]=len[p]+1,_link[clone]=_link[q];
for(int i=0;i<=1;i++) ch[clone][i]=ch[q][i];
while(p!=-1 && ch[p][c]==q){
ch[p][c]=clone;
p=_link[p];
}
_link[q]=_link[cur]=clone;
}
}
return cur;
}
int mx[N];
int dp[N];
int n;
int st[N],sl,sr;
bool check(int L){
memset(dp,0,sizeof(int)*(n+1));
sl=1,sr=0;
for(int i=1;i<=n;i++){
if(i-L>=0){
while(sl<=sr && dp[i-L]-(i-L)>dp[st[sr]]-st[sr]) sr--;
st[++sr]=i-L;
}
while(st[sl]<i-mx[i] && sl<=sr) sl++;
dp[i]=dp[i-1];
if(sl<=sr) dp[i]=max(dp[i-1],dp[st[sl]]-st[sl]+i);
}
if(dp[n]*10>=n*9) return 1;
return 0;
}
signed main(){
int Q,m;
scanf("%d%d",&Q,&m);
for(int i=1;i<=m;i++){
scanf("%s",a+1);
ins(a);
}
for(int i=0;i<=1;i++) if(tr[0][i]) q.push(tr[0][i]);
init();
while(!q.empty()){
int x=q.front();
q.pop();
last[x]=insert(last[fa[x]],str[x]);
for(int i=0;i<=1;i++) if(tr[x][i]) q.push(tr[x][i]);
}
int ans=N;
for(int op=1;op<=Q;op++){
scanf("%s",a+1);
int p=0;
n=strlen(a+1);
for(int i=1;i<=n;i++){
int c=a[i]-'0';
if(ch[p][c]){// 注意 到达的点未必是 len[q]=len[p]+1
mx[i]=mx[i-1]+1;
p=ch[p][c];
}
else{
while(p!=-1 && !ch[p][c]) p=_link[p];
if(p==-1) p=0,mx[i]=0;
else{
mx[i]=len[p]+1;
p=ch[p][c];
}
}
}
int l=0,r=n;
while(l<r){
int mid=(l+r+1)/2;
if(check(mid)) l=mid;
else r=mid-1;
}
printf("%d\n",l);
}
}
广义 SAM
就是求多个串的
首先建一课
code
void ins(char *s){
int n=strlen(s+1);
int u=0;
for(int i=1;i<=n;i++){
int c=s[i]-'a';
if(!tr[u][c]) tr[u][c]=++idx;
fa[tr[u][c]]=u,str[tr[u][c]]=c;
u=tr[u][c];
}
}
int _link[N],ch[N][26],len[N],last[N],tot;
void init(){
_link[0]=-1;
tot=0;
}
int insert(int las,int c){
int cur=++tot;
len[cur]=len[las]+1;
int p=las;
while(p!=-1 && !ch[p][c]){
ch[p][c]=cur;
p=_link[p];
}
if(p==-1) _link[cur]=0;
else{
int q=ch[p][c];
if(len[q]==len[p]+1) _link[cur]=q;
else{
int clone=++tot;
len[clone]=len[p]+1,_link[clone]=_link[q];
for(int i=0;i<26;i++) ch[clone][i]=ch[q][i];
while(p!=-1 && ch[p][c]==q){
ch[p][c]=clone;
p=_link[p];
}
_link[q]=_link[cur]=clone;
}
}
return cur;
}
int main{
for(int i=0;i<26;i++) if(tr[0][i]) q.push(tr[0][i]);
init();
while(!q.empty()){
int x=q.front();
q.pop();
last[x]=insert(last[fa[x]],str[x]);
for(int i=0;i<26;i++){
if(tr[x][i]){
q.push(tr[x][i]);
}
}
}
}
多个串的 LCS#
我们需要对每个节点建立一个长度为
字典树插入的时候将被操作的点标记,然后可以发现在后缀树上,这个节点可以被它子树里面所有的标记所标记。所以
code
#include<bits/stdc++.h>
using namespace std;
const int N=2*1e6+5;
char s[15][N];
queue<int> q;
bitset<20> v[N],w[N];
int tr[N][26],idx=0,fa[N],str[N];
void ins(char *s){
int n=strlen(s+1);
int u=0;
for(int i=1;i<=n;i++){
int c=s[i]-'a';
if(!tr[u][c]) tr[u][c]=++idx;
fa[tr[u][c]]=u,str[tr[u][c]]=c;
u=tr[u][c];
}
}
int _link[N],ch[N][26],len[N],last[N],tot;
void init(){
_link[0]=-1;
tot=0;
}
int insert(int las,int c){
int cur=++tot;
len[cur]=len[las]+1;
int p=las;
while(p!=-1 && !ch[p][c]){
ch[p][c]=cur;
p=_link[p];
}
if(p==-1) _link[cur]=0;
else{
int q=ch[p][c];
if(len[q]==len[p]+1) _link[cur]=q;
else{
int clone=++tot;
len[clone]=len[p]+1,_link[clone]=_link[q];
for(int i=0;i<26;i++) ch[clone][i]=ch[q][i];
while(p!=-1 && ch[p][c]==q){
ch[p][c]=clone;
p=_link[p];
}
_link[q]=_link[cur]=clone;
}
}
return cur;
}
int head[N*2],ver[N*2],nex[N*2],tot_1=0;
void add(int x,int y){
ver[++tot_1]=y,nex[tot_1]=head[x],head[x]=tot_1;
}
int ans=0;
int ps=0;
void dfs(int x){
for(int i=head[x];i;i=nex[i]){
int y=ver[i];
dfs(y);
v[x]|=v[y];
}
if(v[x].count()==ps) ans=max(ans,len[x]);
}
signed main(){
while(scanf("%s",s[++ps]+1)!=EOF){
ins(s[ps]);
}
ps--;
for(int i=0;i<26;i++) if(tr[0][i]) q.push(tr[0][i]);
init();
while(!q.empty()){
int x=q.front();
q.pop();
last[x]=insert(last[fa[x]],str[x]);
for(int i=0;i<26;i++){
if(tr[x][i]){
q.push(tr[x][i]);
}
}
}
for(int q=1;q<=ps;q++){
int u=0;
int n=strlen(s[q]+1);
v[u]|=(1<<q);
for(int i=1;i<=n;i++){
int c=s[q][i]-'a';
u=ch[u][c];
v[u]|=(1<<q);
}
}
for(int i=1;i<=tot;i++){
add(_link[i],i);
}
dfs(0);
printf("%d",ans);
}
杂题
事情的相似度#
枚举右端点,然后往上跳
最坏复杂度
考虑怎么优化跳
SvT#
感觉
你的名字#
先考虑区间在
在考虑在
最后如何求
谢特#
后缀树有一个性质就是,两个前缀的最长公共后缀就是后缀树上
所以考虑每个节点作为
参考资料#
作者:bloss
出处:https://www.cnblogs.com/jinjiaqioi/p/17933054.html
版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效