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\) 小的子串。

有两种询问:

  1. 每个相同的不同位置的子串算一个
  2. 每个相同的不同位置的子串算多个

首先对着 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!迫真

posted @ 2021-01-19 17:57  MontesquieuE  阅读(106)  评论(0编辑  收藏  举报