SAM 的一些用法
SAM 的基操就不总结了,主要记录一下细节容易弄错的用法
1. 全串匹配
简单来说就是把 SAM 当作 ACAM 来用。
假设需要知道匹配串 \(T\) 在文本串 \(S\) 中的匹配情况,则对 \(S\) 建 SAM,枚举 \(T\) 的每一个字符进行匹配的同时维护当前匹配长度 \(len\) 和匹配节点 \(u\)。具体地,当拿到字符 \(c\):
-
存在 \(st[u]\rightarrow to[c]\),则 \(len++\),\(u=st[u]\rightarrow to[c]\)
-
不存在 \(st[u]\rightarrow to[c]\),则不断跳 \(link\) 直到存在或者到达根节点
-
如果存在,先令 \(len=st[u]\rightarrow len+1\),然后 \(u=st[u]\rightarrow to[c]\)
-
如果到了根节点仍然不存在,令 \(len=0\)
代码:
int u=0,len=0;
F(i,0,(int)t.length()-1){
int c=t[i]-'a';
if(st[u].to[c])len++,u=st[u].to[c];
else{
while(u&&!st[u].to[c])u=st[u].link;
len=st[u].len;
if(st[u].to[c])len++,u=st[u].to[c];
}
pre[i+1]=len;//pre数组表示t的每一个前缀的最长可匹配后缀
}
2. 区间匹配
初始给定 \(S\),每次给定 \(T_i\),\(l_i\),\(r_i\) 并询问 \(T_i\) 在 \(S[l_i,r_i]\) 的匹配情况。
仍然对 \(S\) 建出 SAM,此时需要额外处理 SAM 每一个节点的 endpos 集合,在 parent tree 上线段树合并即可。
接下来枚举每个 \(T_i\) 的字符,同时维护当前匹配长度 \(len\) 和匹配节点 \(u\):
-
若存在 \(st[u]\rightarrow to[c]\) 且存在 \(st[st[u]\rightarrow to[c]]\rightarrow endpos[l_i+len\sim r_i]\) 则进行转移,\(len++\),\(u=st[u]\rightarrow to[c]\) 并退出
-
否则不断令 \(len--\) 并进行操作1
-
在这个过程中,一旦发现 \(st[st[u]\rightarrow link]\rightarrow len=len\),立即跳 link
-
如果直到 \(len=0\) 都没有转移,则推出退出。
代码:
-
维护 endpos 集合:
struct state{int to[26],link,len,rt;}st[N<<1]; int sz,lst,tot; string s; inline void insert(int c){ int cur=++sz,p=lst; st[cur].len=st[lst].len+1,lst=cur; while(p!=-1&&!st[p].to[c])st[p].to[c]=cur,p=st[p].link; if(p==-1)return; int q=st[p].to[c]; if(st[p].len+1==st[q].len)return st[cur].link=q,void(); int clone=++sz; st[clone]=st[q],st[clone].len=st[p].len+1,st[clone].rt=0; while(p!=-1&&st[p].to[c]==q)st[p].to[c]=clone,p=st[p].link; st[cur].link=st[q].link=clone; } struct node{int l,r;}T[N<<6]; inline void addsum(int &p,int l,int r,int x){ p=++tot; if(l==r)return; if(x<=mid)addsum(T[p].l,l,mid,x); if(x>mid)addsum(T[p].r,mid+1,r,x); } inline int merge(int x,int y,int l,int r){ if(!x||!y)return x|y; if(l==r)return x; int p=++tot; T[p].l=merge(T[x].l,T[y].l,l,mid); T[p].r=merge(T[x].r,T[y].r,mid+1,r); return p; } inline bool getsum(int p,int l,int r,int L,int R){ if(!p)return 0; if(L<=l&&r<=R)return 1; bool res=0; if(L<=mid)res=getsum(T[p].l,l,mid,L,R); if(R>mid)res|=getsum(T[p].r,mid+1,r,L,R); return res; } vector<int>t[N<<1]; inline void dfs(int u){for(int v:t[u])dfs(v),st[u].rt=merge(st[u].rt,st[v].rt,1,L);} int main(){ ios::sync_with_stdio(0),cin.tie(0),cout.tie(0); cin>>s,L=(int)s.length(); st[0].link=-1; F(i,0,L-1)insert(s[i]-'a'),addsum(st[lst].rt,1,L,i+1); F(i,1,sz)t[st[i].link].push_back(i); dfs(0); } -
进行匹配
string t; int l,r,pre[N]; cin>>t>>l>>r; int u=0,len=0; F(i,0,(int)t.length()-1){ int c=t[i]-'a'; while(1){ if(st[u].to[c]&&getsum(st[st[u].to[c]].rt,1,L,l+len,r)){len++,u=st[u].to[c];break;} if(!len)break; len--; if(len==st[st[u].link].len)u=st[u].link; } pre[i+1]=len;//pre含义同上 }
3. 使用 SAM 构建后缀树
倒着加入 SAM 即可。
虽然操作简单,但是有的时候后缀树很好用,相当于对所有后缀做 trie 然后进行了极致的压缩。
4. 广义 SAM
只介绍离线 bfs 方法,还是比较通用的。
先建 trie,注意额外维护每个节点的父亲和父亲到自己的转移字母。
对 trie 进行 bfs 同时建 SAM 注意这里的 lst 要使用父亲节点的 lst,具体看代码。
int n,tot,pos[N],sz;
string s;
struct trie{int to[26],fa,c;}t[N];
struct state{int to[26],len,link;}st[N<<1];
inline int insert(int c,int lst){
int cur=++sz,p=lst;
st[cur].len=st[p].len+1;
while(p!=-1&&!st[p].to[c])st[p].to[c]=cur,p=st[p].link;
if(p==-1)return cur;
int q=st[p].to[c];
if(st[p].len+1==st[q].len)return st[cur].link=q,cur;
int clone=++sz;
st[clone]=st[q],st[clone].len=st[p].len+1;
while(p!=-1&&st[p].to[c]==q)st[p].to[c]=clone,p=st[p].link;
return st[cur].link=st[q].link=clone,cur;
}
int main(){
cin>>n;
F(i,1,n){
cin>>s;
int u=0;
F(j,0,(int)s.length()-1){
int c=s[j]-'a',&v=t[u].to[c];
if(!v)v=++tot,t[v].fa=u,t[v].c=c;
u=v;
}
}
st[0].link=-1;
queue<int>q;
F(i,0,25)if(t[0].to[i])q.push(t[0].to[i]);
while(q.size()){
int u=q.front();q.pop();
pos[u]=insert(t[u].c,pos[t[u].fa]);
F(i,0,25)if(t[u].to[i])q.push(t[u].to[i]);
}
}

浙公网安备 33010602011771号