**~~
广义SAM总结
**~~
性质同普通SAM,相对普通单串SAM而言,广义SAM用于处理多串的问题。即插入多个串构建SAM
具体有在线离线两种方式。
离线:多串建Trie,在Trie上bfs插入节点
模板
struct Trie{
int root,siz;
int nxt[MAX_N][26],fa[MAX_N],ch[MAX_N];//ch[i]节点i对应的字符
void insert(char *s)
{
int len=strlen(s+1),now=root;
repi(i,1,len){
int c=s[i]-'a';
if(!nxt[now][c]) nxt[now][c]=++siz,fa[siz]=now,ch[siz]=c;
now=nxt[now][c];
}
}
void init()
{
root=siz=1;
ms(nxt[root]);
}
};
struct SAM{
Trie tr;
int root,siz;
int nxt[MAX_N<<1][26],fa[MAX_N<<1],ml[MAX_N<<1],pos[MAX_N<<1];//pos[i] trie上节点i在SAM上对应节点编号
queue<int> q;
void init()
{
root=siz=1;
ml[root]=fa[root]=0;
ms(nxt[root]);
}
int add(int c,int last)
{
int p=last,np=++siz; ms(nxt[np]);
last=np,ml[np]=ml[p]+1;
for(;p&&!nxt[p][c];p=fa[p]) nxt[p][c]=np;
if(!p) fa[np]=root;
else{
int q=nxt[p][c];
if(ml[p]+1==ml[q]) fa[np]=q;
else{
int nq=++siz;
ml[nq]=ml[p]+1;
memcpy(nxt[nq],nxt[q],sizeof(nxt[q]));
fa[nq]=fa[q],fa[q]=fa[np]=nq;
for(;p&&nxt[p][c]==q;p=fa[p]) nxt[p][c]=nq;
}
}
return np;
}
void build()
{
init();
repi(i,0,25)if(tr.nxt[tr.root][i]) q.push(tr.nxt[tr.root][i]);
pos[tr.root]=root;
while(!q.empty()){
int now=q.front(); q.pop();
pos[now]=add(tr.ch[now],pos[tr.fa[now]]);
repi(i,0,25)if(tr.nxt[now][i]) q.push(tr.nxt[now][i]);
}
}
}sam;
/*
sam.tr.init();
repi(i,1,n) ss(s+1),sam.tr.insert(s);
sam.build();
*/
在线:每次插入新串时从root开始,并额外加一些判断。
struct SAM{
int root,siz;
int nxt[MAX_N<<1][26],fa[MAX_N<<1],ml[MAX_N<<1];
void init()
{
root=siz=1;
ml[root]=fa[root]=0;
ms(nxt[root]);
}
int add(int c,int last)
{
if(nxt[last][c]&&ml[last]+1==ml[nxt[last][c]]) return nxt[last][c];
int p=last,np=++siz,nq; ms(nxt[np]);
ml[np]=ml[p]+1;
bool flag=false;
for(;p&&!nxt[p][c];p=fa[p]) nxt[p][c]=np;
if(!p) fa[np]=root;
else{
int q=nxt[p][c];
if(ml[p]+1==ml[q]) fa[np]=q;
else{
if(ml[p]+1==ml[np]) flag=true;
nq=++siz,ml[nq]=ml[p]+1;
memcpy(nxt[nq],nxt[q],sizeof(nxt[q]));
fa[nq]=fa[q],fa[q]=fa[np]=nq;
for(;p&&nxt[p][c]==q;p=fa[p]) nxt[p][c]=nq;
}
}
return flag?nq:np;
}
void insert(char *s)
{
int last=root,len=strlen(s+1);
repi(i,1,len) last=add(s[i]-'a',last);
}
}sam;
对与处理Right集合方面,在线处理能处理一些离线不能解决的问题。一般来说离线能解决在线肯定也能,另外显然在线的空间花费少于离线。所以我要离线有什么用
对于维护Right集合,有两种常见的处理模式。
一种是对多个串每个单独/整体串 维护一个Right。如下
//以下为对每个单独维护的情况
struct SAM{
int Right[MAX_N<<1][2],cnt[MAX_N<<1],id[MAX_N<<1];//Right第二维根据加了多少串来看,后续toposort等位置也要相应作出改动
int add(int c,int last,int id)
{
if(nxt[last][c]&&ml[last]+1==ml[nxt[last][c]]){
Right[nxt[last][c]][id]=1; return nxt[last][c];//若对多个串同时维护Right,此处为++
}
int p=last,np=++siz,nq; ms(nxt[np]);
ml[np]=ml[p]+1,Right[np][id]=0;
bool flag=false;
for(;p&&!nxt[p][c];p=fa[p]) nxt[p][c]=np;
if(!p) fa[np]=root;
else{
int q=nxt[p][c];
if(ml[p]+1==ml[q]) fa[np]=q;
else{
if(ml[p]+1==ml[np]) flag=true;
nq=++siz,ml[nq]=ml[p]+1,Right[nq][id]=0;
memcpy(nxt[nq],nxt[q],sizeof(nxt[q]));
fa[nq]=fa[q],fa[q]=fa[np]=nq;
for(;p&&nxt[p][c]==q;p=fa[p]) nxt[p][c]=nq;
}
}
Right[flag?nq:np][id]=1;
return flag?nq:np;
}
void insert(char *s,int id)
{
int last=root,len=strlen(s+1);
repi(i,1,len) last=add(s[i]-'a',last,id);
}
void toposort()
{
int ML=0;
repi(i,1,siz) cnt[i]=0;
repi(i,1,siz) cnt[ml[i]]++,ML=max(ML,ml[i]);
repi(i,1,ML) cnt[i]+=cnt[i-1];
repi(i,1,siz) id[cnt[ml[i]]--]=i;
repd(i,siz,1)repi(j,0,1) Right[fa[id[i]]][j]+=Right[id[i]][j];
}
}
另一种是插入多个串,Right维护该节点在几个串中出现过
int add(int c,int last,int id)
{
if(nxt[last][c]&&ml[last]+1==ml[nxt[last][c]]){
int tmp=nxt[last][c];
while(tmp&&vis[tmp]!=id) vis[tmp]=id,Right[tmp]++,tmp=fa[tmp];
return nxt[last][c];
}
int p=last,np=++siz,nq; ms(nxt[np]);
ml[np]=ml[p]+1;
bool flag=false;
for(;p&&!nxt[p][c];p=fa[p]) nxt[p][c]=np;
if(!p) fa[np]=root;
else{
int q=nxt[p][c];
if(ml[p]+1==ml[q]) fa[np]=q;
else{
if(ml[p]+1==ml[np]) flag=true;
nq=++siz,ml[nq]=ml[p]+1;
vis[nq]=vis[q],Right[nq]=Right[q];//新建节点继承子点状态
memcpy(nxt[nq],nxt[q],sizeof(nxt[q]));
fa[nq]=fa[q],fa[q]=fa[np]=nq;
for(;p&&nxt[p][c]==q;p=fa[p]) nxt[p][c]=nq;
}
}
int tmp=(flag?nq:np);
while(tmp&&vis[tmp]!=id) vis[tmp]=id,Right[tmp]++,tmp=fa[tmp];
return flag?nq:np;
}
例题:
1.P6139 给定n个小写字母组成的串,求本质不同子串个数
模板题 建广义SAM,利用性质对每个节点统计即可
ll cal()
{
ll ans=0;
repi(i,1,siz) ans+=ml[i]-ml[fa[i]];
return ans;
}
2.P3181 给定两个字符串,求出在两个字符串中各取出一个子串使得这两个子串相同的方案数。两个方案不同当且仅当这两个子串中有一个位置不同。
对两个串建广义SAM,分别维护一个Right集合,对每个结点统计即可。即该节点串类在第一个串的出现次数 * 第二个串的次数 * 串类代表的串数
void toposort()
{
int ML=0;
repi(i,1,siz) cnt[i]=0;
repi(i,1,siz) cnt[ml[i]]++,ML=max(ML,ml[i]);
repi(i,1,ML) cnt[i]+=cnt[i-1];
repi(i,1,siz) id[cnt[ml[i]]--]=i;
repd(i,siz,1)repi(j,0,1) Right[fa[id[i]]][j]+=Right[id[i]][j];
}
ll cal()
{
ll ans=0;
repi(i,1,siz) ans+=1ll*Right[i][0]*Right[i][1]*(ml[i]-ml[fa[i]]);
return ans;
}
3.P3346 一棵树,不定根,每个节点上有一个字符,求任意两个节点加上其路径构成的本质不串数(叶子节点少于20)
枚举根插入串,再对每个节点统计即可
struct SAM{
~~~
ll cal()
{
ll res=0;
repi(i,1,siz) res+=1ll*(ml[i]-ml[fa[i]]);
return res;
}
}sam;
void dfs(int s,int fa,int last)
{
last=sam.add(c[s],last);
reps(s)if(e[i].to!=fa) dfs(e[i].to,s,last);
}
int main()
{
si(n),si(cc);
init(n);
repi(i,1,n) si(c[i]);
repi(i,1,n-1){
int u,v; si(u),si(v);
add_edge(u,v),add_edge(v,u);
in[u]++,in[v]++;
}
sam.init();
repi(i,1,n)if(in[i]==1) dfs(i,0,sam.root);
printf("%lld\n",sam.cal());
return 0;
}
4.2019ICPC徐州现场赛-L
给定一棵树,定根。每个节点有一个字符。询问从节点x向上l个字符到达y,y-x路径构成一个串,问有几个节点也可以构成相同串(x本身也计入)
首先对整体建SAM,记录树上每个节点加入后对应SAM中的节点,维护整体Right集合。对每次查询,首先跳到查询的结尾点在SAM上对应的结尾点,后沿fa向上跳到ml[fa]小于要求的匹配长度,不能跳的时候所在的节点的Right大小即为所求。
struct SAM{
void toposort()
{
int ML=0;
repi(i,1,siz) cnt[i]=0;
repi(i,1,siz) cnt[ml[i]]++,ML=max(ML,ml[i]);
repi(i,1,ML) cnt[i]+=cnt[i-1];
repi(i,1,siz) id[cnt[ml[i]]--]=i;
repd(i,siz,1) Right[fa[id[i]]]+=Right[id[i]];
}
int cal(int pos,int l)
{
while(ml[fa[pos]]>=l) pos=fa[pos];
return Right[pos];
}
}sam;
int pos[MAX_N];
void dfs(int s,int last)
{
pos[s]=last=sam.add(c[s],last);
reps(s) dfs(e[i].to,last);
}
int n,q;
char s[MAX_N];
int main()
{
si(n),si(q),ss(s+1);
init(n);
repi(i,1,n) c[i]=s[i]-'A';
repi(i,2,n){
int fa; si(fa);
add_edge(fa,i);
}
sam.init(),dfs(1,sam.root);
sam.toposort();
while(q--)
{
int x,l; si(x),si(l);
printf("%d\n",sam.cal(pos[x],l));
}
return 0;
}
5.bzoj2780 有n个大串和m个询问,每次给出一个字符串s询问在多少个大串中出现过
对每个节点Right记录该集合对应串在几个给定大串中出现过(上面的第二种Right)
每次插入单独维护,并更新到Right中,对查询串在SAM上走到最后返回对应节点的Right大小即可
void insert(char *s,int id)
{
int last=root,len=strlen(s+1);
repi(i,1,len) last=add(s[i]-'a',last,id);
}
int cal(char *s)
{
int now=root,len=strlen(s+1);
repi(i,1,len){
int c=s[i]-'a';
if(nxt[now][c]) now=nxt[now][c];
else return 0;
}
return Right[now];
}
6.bzoj3277 现在给定你n个字符串,询问每个字符串有多少子串(不包括空串)是所有n个字符串中至少k个字符串的子串(注意包括本身)
所有子串-该串的所有前缀的所有后缀,按拓扑序把每个节点权值更新,父传子,对每个串每个前缀贡献求和即可
struct SAM{
ll w[MAX_N<<1];
void toposort()
{
int ML=0;
repi(i,1,siz) cnt[i]=0,w[i]=1ll*((Right[i]>=k)?(ml[i]-ml[fa[i]]):0);
repi(i,1,siz) cnt[ml[i]]++,ML=max(ML,ml[i]);
repi(i,1,ML) cnt[i]+=cnt[i-1];
repi(i,1,siz) id[cnt[ml[i]]--]=i;
repi(i,1,siz) w[id[i]]+=w[fa[id[i]]];
}
ll cal(char *s,int id)
{
ll ans=0;
int len=strlen(s+1),now=root;
repi(i,1,len){
int c=s[i]-'a';
now=nxt[now][c];
ans+=w[now];
}
return ans;
}
}sam;
char s[MAX_N<<4];
int st[MAX_N];
int main()
{
sam.init();
si(n),si(k);
st[1]=0;
repi(i,1,n){
ss(s+st[i]+1),sam.insert(s+st[i],i);
st[i+1]=strlen(s+st[i]+1)+2+st[i];
}
sam.toposort();
repi(i,1,n) printf("%lld%c",sam.cal(s+st[i],i),ce(i,n));
return 0;
}
暂时到这,后续做完再补充一些套数据结构的题
开始补作业