S A M SAM SAM题型总结

前言:菜死了,菜死了。

插入模板:

struct SAM{
	int last,cnt,ch[N][26],fa[N],sz[N];
	void ins(int c){
		int p=last,np=++cnt;len[np]=len[p]+1;
		while(;p&&!ch[p][c];p=fa[p]) ch[p][c]=np;
		if(!p) fa[np]=1;
		else {
			int q=ch[p][c];
			if(len[q]==len[p]+1) fa[np]=q;
			else { int nq=++cnt;
				  len[nq]=len[p]+1;
				  memcyp(ch[nq],ch[q],sizeof ch[q]);
				  fa[nq]=fa[q],fa[q]=fa[p]=nq;
				  while(;ch[p][c]==q;p=fa[p]) ch[p][c]=nq;
			}
		}sz[np]=1;
	}
}; 

拓扑模板:

for(int i=1;i<=cnt;i++) b[len[i]]++;
	for(int i=1;i<=cnt;i++) b[i]+=b[i-1];
	for(int i=1;i<=cnt;i++) a[b[len[i]]--]=i;
	for(int i=cnt;i;i--){
		int p=a[i];
	//	dp[p]=1;for(int j=0;j<26;j++) if(ch[p][j]) dp[p]+=dp[ch[p][j]];	 求从i开始自动机的子串个数 
	//	sz[fa[p]]+=sz[p]; 求endpos 集合子串出现次数 
	}

1.求本质不同的子串个数。

法1:考虑拓扑排序后 d p dp dp,因为 S A M SAM SAM上从任意点到任意点都是原串的子串。

所以令 d p [ i ] dp[i] dp[i]表示从第 i i i个状态出发的本质不同子串数,然后根据边转移即可。

for(int i=1;i<=cnt;i++) b[len[i]]++;
		for(int i=1;i<=cnt;i++) b[i]+=b[i-1];
		for(int i=1;i<=cnt;i++) a[b[len[i]]--]=i;
		for(int i=cnt;i;i--){
			int p=a[i];
			dp[p]=1;
			for(int i=0;i<26;i++) if(ch[p][i]) dp[p]+=dp[ch[p][i]];
		}
	printf("%lld\n",dp[1]-1);

法2:考虑每次添加一个字符增加的子串数,显然对于当前状态 i i i,属于 e n d p o s i endpos_i endposi这个集合的子串就是增加的子串数。因为这个集合的子串长度都是连续且唯一的,长度变化从 [ m i n l e n , m a x l e n ] [minlen,maxlen] [minlen,maxlen],令 l e n [ i ] = m a x l e n len[i]=maxlen len[i]=maxlen,显然 m i n l e n = l e n [ f a [ i ] ] + 1 minlen=len[fa[i]]+1 minlen=len[fa[i]]+1,所以增加的子串数位: m a x l e n − m i n l e n + 1 = l e n [ i ] − l e n [ f a [ i ] ] maxlen-minlen+1=len[i]-len[fa[i]] maxlenminlen+1=len[i]len[fa[i]]

最终求和即可。

for(int i=1;i<=cnt;i++) ans+=len[i]-len[fa[i]];
		printf("%lld\n",ans);

2.求长度为 i i i的子串出现的最大次数。

思路:因为每个状态 e n d p o s endpos endpos集合的子串的出现次数是相同的,我们可以用拓扑 d p dp dp,对应状态子串集合的出现次数。
然后用一个数组
a n s [ ] , a n s [ l e n [ i ] = m a x ( a n s [ l e n [ i ] , s z [ i ] ) ans[],ans[len[i]=max(ans[len[i],sz[i]) ans[],ans[len[i]=max(ans[len[i],sz[i]),来更新长度 i i i对应的最长的子串的最大出现次数。
又因为前一个串是后一个串的后缀,所以最后我们还需倒着更新一下
a n s [ i ] = m a x ( a n s [ i ] , a n s [ i + 1 ] ) ans[i]=max(ans[i],ans[i+1]) ans[i]=max(ans[i],ans[i+1])

for(int i=1;i<=cnt;i++) b[len[i]]++;
		for(int i=1;i<=cnt;i++) b[i]+=b[i-1];//求前缀和. 
		for(int i=1;i<=cnt;i++) a[b[len[i]]--]=i;	//类似桶排 将结点长度从小到大排序. 
		for(int i=cnt;i;i--){	//从长度大的开始遍历,跑拓扑序. 
			int p=a[i];	//因为长度大的结点的fail结点指向的后缀也是长度大的后缀
			sz[fa[p]]+=sz[p];//状态转移. 
		//	if(sz[p]>1) ans=max(ans,1LL*sz[p]*len[p]);//更新答案. 
		}
for(int i=1;i<=cnt;i++) ans[len[i]]=max(ans[len[i]],sz[i]);
		for(int i=n-1;i;i--) ans[i]=max(ans[i],ans[i+1]);
		for(int i=1;i<=n;i++) printf("%d\n",ans[i]);

3.求模式串是否在文本串出现过。

用文本串建立 S A M SAM SAM,然后模式串跑 S A M SAM SAM即可。

	scanf("%s",t+1);int p=1;
			for(int i=1;t[i];i++){
				int c=t[i]-'a';
				if(ch[p][c]) p=ch[p][c];
				else return false;
			}return true;
		}

4.求两个字符串的最长公共子串长度。

思路:一个字符串在另一个字符串上跑 S A M SAM SAM,每次更新最大长度。

int ans=0,p=1,res=0;
		for(int i=1;b[i];i++){
			int x=b[i]-'a';
			if(ch[p][x]) p=ch[p][x],ans++;
			else {
				while(p&&!ch[p][x]) p=fa[p];
				if(!p) p=1,ans=0;
				else ans=len[p]+1,p=ch[p][x];
			}
			if(res<ans) res=ans;//以b[i]结尾子串的最大LCM长度
		}
		printf("%d\n",res);

5.求字典序第 k k k小的子串。

s z [ i ] sz[i] sz[i] 为结点 i i i对应的 e n d p o s endpos endpos集合的每个子串的出现次数。

s u m [ i ] sum[i] sum[i] i i i出发的自动机的子串数量。

思路:1.若子串为本质不同,则 s z [ i ] = 1 sz[i]=1 sz[i]=1,每个状态对应的 e n d p o s endpos endpos集合的子串出现次数为 1 1 1
2.否则,按长度状态转移求出 s z [ ] sz[] sz[]

if(!t) for(int i=cnt;i;i--) sum[i]=sz[i]=1;//sz[1]=0; 本质不同的子串 
		else for(int i=cnt;i;i--) sum[i]=sz[i];
		sum[1]=sz[1]=0;	//位置不同也算不同 

然后令数组 s u m [ i ] sum[i] sum[i]表示从 i i i出发的子串个数。

for(int i=cnt;i;i--)
			for(int j=0;j<26;j++) if(ch[a[i]][j]) sum[a[i]]+=sum[ch[a[i]][j]];

然后跑 d f s dfs dfs

void Print(int p,int k){
		if(k<=sz[p]) return;
		k-=sz[p];
		for(int i=0;i<26;i++)
			if(ch[p][i]){
				int q=ch[p][i];
				if(k>sum[q]) k-=sum[q];
				else {
					putchar('a'+i),Print(q,k);return; 
				}
			}
	}