SAM 练习记录
不同子串个数
求给定串的本质不同子串个数。
如果你看过 Flying2018 的讲解 ,就会知道这题有个极其简便的做法QWQ
注意到一个字符串唯一对应了一个 Parent Tree 上的节点,而一个节点表示的是长度为 \(len_u\) 的前缀中长度在 \((len_{fa},len_u]\) 的后缀,所以答案就是对 \(len_u-len_{fa_u}\) 求和。
ll Query()
{
ll ans=0;
for ( int i=1; i<=cnt; i++ )
ans=ans+len[i]-len[fa[i]];
return ans;
}
弦论
求第 \(k\) 小的子串。
有两种询问:
- 每个相同的不同位置的子串算一个
- 每个相同的不同位置的子串算多个
首先对着 SAM 搞一遍拓扑,然后倒序计算出每个节点的 \(siz\) 并统计出子树大小。如果是算一个,那么 \(siz\) 直接为 \(1\) ,否则 \(siz\) 为 \(endpos\) 大小。然后递归找第 \(k\) 小,每次顺序遍历 \(26\) 个字符,找到第一个前缀和 \(\ge k\) 的转移边,然后沿着这个转移边走就好了。
对于判断无解,直接和根的总和比较即可。
SP1811 LCS
求两个字符串的 LCS .
拿第一个串建立 SAM ,然后用第二个串在上面匹配。
设当前位置为 \(p\) ,当前枚举的字符为 \(c\) ,如果 \(tr[p][c]\) 存在,那么直接累计;
否则,往祖先跳,如果找到了存在 \(c\) 转移边的祖先,那么走 \(len[p]+1\) ;如果找不到,就直接从 \(0\) 开始匹配,每次取 \(\max\) 即可。
int Query()
{
int p=1,c,res=0,ans=0;
for ( int i=1; i<=n; i++ )
{
c=s[i]-'a';
if ( tr[p][c] ) res++,p=tr[p][c];
else
{
for ( ; p && !tr[p][c]; p=fa[p] ) ;
if ( p ) res=len[p]+1,p=tr[p][c];
else res=0,p=1;
}
ans=max( ans,res );
}
return ans;
}
SP1812 LCS2
求多个串的 LCS .
显然不能按照两个串跑然后取 \(\min\) ,但是可以参照这个思路。
依然用第一个串建 SAM ,后面的串在上面跑。注意到如果一个串和某个节点匹配上了,那么显然这个节点的所有祖先都能匹配上。所以每个节点统计的时候应该是本身的值和子节点取 \(\max\) ,和自己的 \(len\) 取 \(\min\) .
对于统计,再在每个节点上统计一个当前节点的最小匹配值,等所有串都跑完之后,将 \(ans\) 和每个节点的最小值取 \(\max\) 即可。
void Init()
{
for ( int i=1; i<=cnt; i++ ) buc[len[i]]++;
for ( int i=1; i<=n; i++ ) buc[i]+=buc[i-1];
for ( int i=1; i<=cnt; i++ ) ord[buc[len[i]]--]=i;
for ( int i=1; i<=cnt; i++ )
mat[i]=0,mnmat[i]=len[i];
}
void Get_Match()
{
int p=1,c,res=0;
for ( int i=1; i<=n; i++ )
{
c=s[i]-'a';
while ( p && !tr[p][c] ) p=fa[p],res=len[p];
if ( p ) res++,p=tr[p][c],bmax(mat[p],res);
else p=1,res=0;
}
for ( int i=cnt; i; i-- )
{
p=ord[i]; bmax( mat[fa[p]],min(mat[p],len[fa[p]]) );
bmin( mnmat[p],mat[p] ); mat[p]=0;
}
}
int Query()
{
int ans=0;
for ( int i=1; i<=cnt; i++ )
bmax( ans,mnmat[i] );
return ans;
}
生成魔咒
依次在末尾加入字符,实时求本质不同子串个数。
直接在每次插入的时候统计 \(len[i]-len[fa[i]]\) 即可。注意,这里更新的时候只需要更新 \(i=q\) ,不需要更新 \(i=nq\) ,因为 \(nq\) 的位置在 \(np\) 和 \(fa[np]\) 之间,所以会加减抵消,就相当于是 \(len[np]-len[fa[np]]\) 了。
甲苯先生和大中锋的字符串
求字符串中恰好出现了 \(k\) 次的子串中,出现次数最多的长度。
首先要建 SAM 并维护出 \(endpos\) 集合大小。这个直接求子树和就可以了。
如果大小为 \(k\) ,那么 \([minlen,mxlen]\) 都会产生贡献,做一次差分,最后求最大值即可。
注意题目是多测。
CF802I Fake News
求 \(\sum_p cnt(s,p)^2\) ,其中 \(cnt_{s,p}\) 表示子串 \(p\) 在 \(s\) 中的出现次数。
首先计算出 \(endpos\) 集合大小。然后,对于节点 \(x\) ,其贡献为 \(cnt[x]^2\times (mxlen(x)-mnlen(x))\) .
ll Query()
{
for ( int i=1; i<=cnt; i++ ) buc[len[i]]++;
for ( int i=1; i<=n; i++ ) buc[i]+=buc[i-1];
for ( int i=1; i<=cnt; i++ ) ord[buc[len[i]]--]=i;
bool fl=0;
for ( int i=cnt,p=ord[i]; i; i--,p=ord[i] ) siz[fa[p]]+=siz[p];
ll ans=0,res=0;
for ( int i=1; i<=cnt; i++ )
{
res=1ll*siz[i]*siz[i];
ans=ans+res*(len[i]-len[fa[i]]);
}
return ans;
}
诸神眷顾的幻想乡
给定一棵树,求树上两两间路径形成的字符串中,本质不同子串个数。
这道题开始就已经学会广义SAM了。题目中有条件,叶子节点个数不超过 \(20\) 个,所以可以暴力 DFS 找出叶子节点的两两间路径,然后插入广义 SAM ,直接查询就好了。
Standing Out from the Herd P
给定 \(1e5\) 个字符串,长度和 \(\leq 1e5\) ,求对于每个字符串,只属于该串的本质不同非空子串个数。
在 Insert
的时候一边插入一边对每个节点打标记(标记为当前字符串的编号,便于统计),如果已经有标记了就直接赋值 \(-1\) ,那么只需要对所有非 \(-1\) 的节点统计 \(ans\) 即可。
一开始脑子抽了,在广义 SAM 的特判里面打错标记了……显然如果只是前缀而非已有节点的话,标记要打在新复制的那个节点上。
struct Suffix_Automaton
{
int len[N<<1],tr[N<<1][C],fa[N<<1],cnt=1,vis[N<<1],buc[N<<1],ord[N<<1];
ll ans[N];
int Insert( int c,int las,int tag )
{
if ( tr[las][c] )
{
int p=las,np=tr[p][c];
if ( len[p]+1==len[np] ) { vis[np]=-1; return np; }
else
{
int nq=++cnt; len[nq]=len[p]+1; vis[nq]=tag;
memcpy( tr[nq],tr[np],sizeof(tr[np]) );
fa[nq]=fa[np]; fa[np]=nq;
for ( ; p && tr[p][c]==np; p=fa[p] ) tr[p][c]=nq;
return nq;
}
}
int p=las,q=++cnt; len[q]=len[p]+1;
if ( vis[q]==0 ) vis[q]=tag;
else vis[q]=-1;
for ( ; p && !tr[p][c]; p=fa[p] ) tr[p][c]=q;
if ( !p ) fa[q]=1;
else
{
int np=tr[p][c];
if ( len[np]==len[p]+1 ) fa[q]=np;
else
{
int nq=++cnt; memcpy( tr[nq],tr[np],sizeof(tr[nq]) );
fa[nq]=fa[np]; fa[np]=fa[q]=nq; len[nq]=len[p]+1;
for ( ; p && tr[p][c]==np; p=fa[p] ) tr[p][c]=nq;
}
}
return q;
}
void Query()
{
for ( int i=1; i<=cnt; i++ ) ++buc[len[i]];
for ( int i=1; i<=cnt; i++ ) buc[i]+=buc[i-1];
for ( int i=1; i<=cnt; i++ ) ord[buc[len[i]]--]=i;
for ( int i=cnt,p=ord[i]; i; i--,p=ord[i] )
{
if ( vis[p]!=-1 ) ans[vis[p]]+=len[p]-len[fa[p]];
if ( vis[fa[p]]==0 ) vis[fa[p]]=vis[p];
else if ( vis[fa[p]]^vis[p] ) vis[fa[p]]=-1;
}
for ( int i=1; i<=n; i++ )
printf( "%lld\n",ans[i] );
}
}sam;
Three Strings
给定三个字符串 \(A,B,C\) ,对于每个 \(L\in[1,\min(len_A,len_B,len_C)]\) ,求满足 \(A[a,a+L-1]=B[b,b+L-1]=C[c+L-1]\) 的三元组 \((a,b,c)\) 的数量,(对每个 \(L\) 给出结果)对 \(1e9+7\) 取模。
简化题意:对于每个三个字符串的公共子串,求出现次数的乘积之和。
由于是要求出现次数乘积,所以在正常插入的同时,要分别维护三种 \(tag\) 的 \(siz\) ,即每次 Insert
额外设置一个参数 \(tag\) ,将 \(A,B,C\) 在节点上的 \(siz\) 分开存储。
后面的事情就和 “甲苯先生” 那题一样了,维护完之后直接差分即可。
struct Suffix_Automaton
{
int len[N<<1],tr[N<<1][C],fa[N<<1],siz[N<<1][3],cnt=1;
int buc[N<<1],ord[N<<1];
ll ans[N],sum[N<<1][3];
int Insert( int c,int las,int tag )
{
if ( tr[las][c] )
{
int p=las,np=tr[p][c];
if ( len[p]+1==len[np] ) { siz[np][tag]++; return np; }
else
{
int nq=++cnt; len[nq]=len[p]+1; siz[nq][tag]++;
memcpy( tr[nq],tr[np],sizeof(tr[np]) );
fa[nq]=fa[np]; fa[np]=nq;
for ( ; p && tr[p][c]==np; p=fa[p] ) tr[p][c]=nq;
return nq;
}
}
int p=las,q=++cnt; len[q]=len[p]+1; siz[q][tag]++;
for ( ; p && !tr[p][c]; p=fa[p] )
tr[p][c]=q;
if ( !p ) fa[q]=1;
else
{
int np=tr[p][c];
if ( len[np]==len[p]+1 ) fa[q]=np;
else
{
int nq=++cnt; memcpy( tr[nq],tr[np],sizeof(tr[nq]) );
fa[nq]=fa[np]; fa[np]=fa[q]=nq;
len[nq]=len[p]+1;
for ( ; p && tr[p][c]==np; p=fa[p] )
tr[p][c]=nq;
}
}
return q;
}
void Query()
{
for ( int i=1; i<=cnt; i++ ) buc[len[i]]++;
for ( int i=1; i<=cnt; i++ ) buc[i]+=buc[i-1];
for ( int i=1; i<=cnt; i++ ) ord[buc[len[i]]--]=i;
for ( int i=cnt,p=ord[i]; i; i--,p=ord[i] )
for ( int j=0; j<3; j++ )
siz[fa[p]][j]+=siz[p][j];
for ( int i=1; i<=cnt; i++ )
{
int p=ord[i];
ll res=1ll*siz[p][0]*siz[p][1]%Mod*siz[p][2]%Mod;
ans[len[fa[p]]+1]+=res; ans[len[p]+1]-=res;
ans[len[fa[p]]+1]%=Mod; ans[len[p]+1]%=Mod;
}
for ( int i=1; i<=n; i++ )
ans[i]+=ans[i-1],ans[i]=(ans[i]%Mod+Mod)%Mod;
for ( int i=1; i<=n; i++ )
printf( "%lld ",ans[i] );
}
}sam;
Sevenk Love Oimaster
给定 \(n\) 个模板串,以及 \(m\) 个查询串,查询每一个查询串是多少个模板串的子串。
先按正常构建把所有模板串插入 SAM,然后再枚举每个串,对于每个字符暴力跳 \(fa\) 打标记,可以证明复杂度是 \(\mathcal{O}(n\sqrt n)\) 的。然后直接在 Trie 上跳询问串就好了。
void Init( char *s,int tag )
{
int l=strlen(s+1),p1=1,p2;
for ( int i=1; i<=l; i++ )
{
p1=tr[p1][s[i]]; p2=p1;
while ( p2 && vis[p2]!=tag ) vis[p2]=tag,siz[p2]++,p2=fa[p2];
}
}
int Query( char *s,int l )
{
int p=1;
for ( int i=1; i<=l; i++ )
{
int c=s[i];
if ( !tr[p][c] ) return 0;
p=tr[p][c];
}
return siz[p];
}
Match & Catch
求两个字符串各自只出现一次的最短公共子串。
把这两个串丢进广义 SAM,然后分别维护 \(endpos\) 大小,如果都为 \(1\) 那么就和 \(minlen\) 取 \(\min\) 即可。
注意不要把根节点统计进去。
熟悉的文章
给定若干字符串,将一个字符串分成若干个连续子串,如果一个子串 \(len\ge L\) 且出现过即合法。如果合法子串总长 \(\ge\) 原串长度的 \(90\%\) ,那么就合法。求使得询问串成为合法的最大 \(L\) .
先把所有文本串丢进广义SAM,然后对于每个询问串,类似 LCS 一样求一遍匹配(后缀),然后再 DP 一遍即可,易知能用单调队列优化。
这道题给了我一个很好的教训:
- 函数传参传字符指针的话,有可能
s+1
会挂。
void Get_Match( char *s,int l )
{
int p=1,res=0,c=0;
for ( int i=0; i<l; i++ )
{
c=s[i]-'0';
while ( (p^1) && !tr[p][c] ) p=fa[p],res=len[p];
if ( tr[p][c] ) p=tr[p][c],res++;
mat[i+1]=res;
}
}
bool Check( int x,int slen )
{
for ( int i=1; i<x; i++ ) f[i]=0;
int head=1,tail=0;
for ( int i=x; i<=slen; i++ )
{
f[i]=f[i-1];
while ( head<=tail && f[q[tail]]-q[tail]<=f[i-x]-i+x ) tail--;
q[++tail]=i-x;
while ( head<=tail && q[head]<i-mat[i] ) head++;
if ( head<=tail ) bmax( f[i],f[q[head]]-q[head]+i );
}
return f[slen]*10>=slen*9;
}
CF235C Cyclical Quest
给定一个主串 \(S\) 和 \(n\) 个询问串,求每个询问串的所有循环同构在主串中出现的次数总和。
循环同构串,直接在后面接一份自己跑匹配即可。
注意当前长度大于串长时要跳 \(fa\) ,并且要记录当前节点是否已经贡献过答案(因为有可能循环同构出一样的串)
void Query()
{
memset( vis,-1,sizeof(vis) ); int q=read();
while ( q-- )
{
scanf( "%s",s+1 ); int l=strlen(s+1);
for ( int i=1; i<l; i++ ) s[i+l]=s[i];
int p=1,mat=0; ll ans=0;
for ( int i=1; i<l+l; i++ )
{
int c=s[i]-'a';
while ( p && !tr[p][c] ) p=fa[p],mat=len[p];
if ( !p ) p=1,mat=0;
else p=tr[p][c],mat++;
while ( p && len[fa[p]]>=l ) p=fa[p],mat=len[p];
if ( mat>=l && vis[p]!=q ) ans+=siz[p],vis[p]=q;
}
printf( "%lld\n",ans );
}
}
封印
给定两个字符串 \(s,t\) ,\(q\) 次询问,求 \(s[l\dots r]\) 和 \(t\) 的 LCS 长度。
既然是在 \(s\) 中挖的区间,显然就要用 \(t\) 建 SAM。和上一题一样预处理出最大匹配长度。
如何求区间 \([l,r]\) 的匹配?显然可以二分匹配长度,Checker 就是在 \([l+mid-1,r]\) 里面找一个 \(mat\) 大于 \(mid\) 的,区间查询最值直接ST表即可。代码也很简洁。
口头禅
给定 \(n\) 个 \(01\) 串,求 \(s[l]\sim s[r]\) 的 LCS。
最后一道纯SAM了,再往后就是恶心DS了QAQ
这题卡常,根号被专门Hack了,但是听说O2+小常数能过去,我就写一写
首先,对于所有串建广义SAM,并维护每个端点的 \(llim,rlim\) ,表示 \(s[1]\sim s[r]\) 中,以这个节点为子串的最右连续区间。直接暴力跳 \(fa\) 维护即可。
将询问离线下来,把 \([l,r]\) 这样的询问挂到 \(r\) 上去。顺序枚举 \(r\) ,并将所有挂在这个 \(r\) 上的询问按照左端点排序,假设当前有 \(q\) 个询问。
对于每个节点 \(p\) ,二分到第一个 \(l[i]\ge llim\) ,对询问 \([i,q]\) 的答案都和 \(len[p]\) 取 \(\max\) ,可以类似差分维护,最后前缀 \(\max\) 一遍即可。
松不动了……一直 TLE
两个点。就不贴代码了QAQ
CF666E Forensic Examination
给定一个字符串 \(S\) 和若干字符串 \(T[1\dots m]\) ,询问 \(S[pl\dots pr]\) 在 \(T[l]\sim T[r]\) 中出现次数最多的串编号及次数。
开始DS了!/jk
首先对所有 \(T\) 串建广义SAM,维护每个节点在每个字符串里面的出现次数,动态开点+线段树合并即可。
对于所有询问,离线并按照 \(r\) 挂询问链,依次在SAM上跑匹配,对于每个右端点,倍增找到最后一个节点使得长度 \(\ge pr-pl+1\) ,然后在线段树上直接查询 \([l,r]\) 区间即可。
DS+SAM 快调成狗了 /kk
CF1037H Security
不写了不写了,孩子玩得很开心,下次做 DS 还来找 SAM!迫真