SAM总结

**~~

SAM总结

**~~
主要用于处理子串相关问题
对于一个建好的SAM,有如下性质
1.有一个根节点,若干个终止点。边(nxt数组)代表在目前的字符串后加上的字母。从源点到任意一个节点的任意路径可以形成一个字符串。这个串表示的一定为原串的子串,即SAM可以判定一个串是否是插入SAM的串的子串。
2.从源点出发到任意终止节点表示的串为原串后缀
3.对于每个节点,其代表一类字符串,即从根节点出发,到这个节点为终止的所有字符串,ml数组记录这个串类中串的最大长度,一个串类中的串长len是一个范围,有最大最小值,中间长度连续。ml中存放最大值。
ml[fa[i]]+1=i节点串类最小长度
因此,对于i节点表示的串类其中代表的子串个数为 ml[i]-ml[fa[i]]
4. 因为插入节点时会有在两个节点中额外加节点的操作,所以处理节点整体Right集合时需要拓扑排序,不能直接倒序加。拓扑排序处理后的Right集合表示的是该串类所有串在插入SAM的串中的出现次数
5. 算法复杂度o(n)
我的SAM模板

/*
root根 节点1-siz  root为1
每个节点表示的不同字串个数=当前节点ml-父节点ml
相关数组双倍大小
拓扑序后Right即为该串类中所有串出现次数
*/
struct SAM{
	int root,last,siz;
	int nxt[MAX_N<<1][26],fa[MAX_N<<1],ml[MAX_N<<1];
	int Right[MAX_N<<1],cnt[MAX_N<<1],id[MAX_N<<1];
	void init()
	{
		root=last=siz=1;
		ml[root]=fa[root]=Right[root]=0;
		ms(nxt[root]);
	}
	void add(int c)
	{
		int p=last,np=++siz; ms(nxt[np]);
		last=np,ml[np]=ml[p]+1,Right[np]=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,Right[nq]=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;
			}
		}
	}
	void build(char *s)
	{
		init();
		int len=strlen(s+1);
		repi(i,1,len) add(s[i]-'a');
	}
	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]];
	}
}sam;

例题:
1.P3804 出现次数不为 11的子串的出现次数乘上该子串长度的最大值。 模板题,建SAM,拓扑排序后对每个节点贡献求和即可

repi(i,1,siz)if(Right[i]>1)	ans=max(ans,1ll*Right[i]*ml[i]);

2.spoj1811 两个串的LCS,用S串建SAM,T串在SAM上跑,取所有经过节点对应匹配长度的最大值即可

int cal(char *s)
	{
		int ans=0,res=0,ls=strlen(s+1);
		int now=root;
		repi(i,1,ls){
			int c=s[i]-'a';
			if(nxt[now][c]) res++,now=nxt[now][c];
			else{
				for(;now&&!nxt[now][c];now=fa[now]);
				if(!now) res=0,now=root;
				else res=len[now]+1,now=nxt[now][c];
			}
			ans=max(ans,res);
		}
		return ans;
	}

3.spoj1812 多串的LCS 对一个串建SAM,其他串依次在SAM上匹配。每个节点额外维护两个值,lcs[]代表该节点对目前已匹配串LCS,nlcs[] 当前正在匹配的串的LCS,
匹配串更新节点时显然取最大值,后按拓扑序对每个节点的lcs更新,注意如果一个节点nlcs[]>0,那么其父节点x的nlcs需要更新为ml[x]

void cal(char *s)
	{
		int ls=strlen(s+1),now=root,len=0;
		repi(i,1,ls){
			int c=s[i]-'a';
			if(nxt[now][c]) len++,now=nxt[now][c];
			else{
				for(;now&&!nxt[now][c];now=fa[now]);
				if(!now) now=root,len=0;
				else len=ml[now]+1,now=nxt[now][c];
			}
			nlcs[now]=max(nlcs[now],len);//对每个串保存对每个结点的最大lcs
		}
		repd(i,siz,1){
			int x=id[i];
			lcs[x]=min(lcs[x],nlcs[x]);//用每个结点的最大更新总体,总体的保留最小串
			if(nlcs[x]&&fa[x]) nlcs[fa[x]]=ml[fa[x]];//可能一个状态转移过来会有fa被略过的情况(额外添加节点时,建立自动机的cas3),此时fa的nlcs可能为0,而实际上因为fa的son状态出现了,fa的nlcs就为对应状态的ml
			nlcs[x]=0;
		}
	}

4.uva719 用SAM解决最小表示法的问题
首先对原串复制一倍加在后面,并整体建SAM
后从根节点贪心的找最小字符向后跑,长度到达原串串长是停止并输出即可。利用SAM的性质:从根节点延边到达任何点路径形成的字符串都是建SAM的串的子串

struct SAM{
~~~~~~
	int dfs(int now,int cnt)
	{
			if(!cnt) return ml[now];
			repi(i,0,25)if(nxt[now][i]) return dfs(nxt[now][i],cnt-1);
	}
}
int main()
{
	int T; si(T);
	while(T--)
	{
		ss(s+1);
		int len=strlen(s+1);
		repi(i,1,len) s[i+len]=s[i];
		s[2*len+1]='\0';
		sam.build(s);
		printf("%d\n",sam.dfs(sam.root,len)-len+1);
	}
	return 0;
}

5.给一个字符串S,令F(x)表示S的所有长度为x的子串中,出现次数的最大值。求F(1)…F(Length(S))
Right的含义
为该节点代表的子串类的出现次数,用一个节点的ml去更新对应长度的最大次数
但考虑由于一个节点实际代表一类长度,且其向上的父节点的次数一定大于等于该节点次数即出现了长度x的子串i次,那么长x-1的子串至少也是i次,拓扑序后再倒着更新一次即可

void solve(int len)
{
	repi(i,0,len) f[i]=0;
	repi(i,1,siz) f[ml[i]]=max(f[ml[i]],Right[i]);
	repd(i,len,0) f[i]=max(f[i],f[i+1]);
	repi(i,1,len) printf("%d\n",f[i]);
}

6.spoj7258 求一个串字典序第k小的子串(重复不计)
sam从root走可跑出所有子串,那么对一个字符,如a,从root走向a,a开头的子串数量即为a后
所有nxt0-25对应的子串数和,按拓扑序倒着更新即可 (此题不用Right和fa)
每个节点本身为1(虽然对应长度可能很多,但向上更新时只取1个,但会取多次,例如可能的长度
范围4-6 加一个字符可转移到改状态的节点相应应该有3个,会分别叠加上去)
处理出每个点对应字符开头包含的子串数目,后按字典序小找字符即可

void toposort()
{
~~~~
	repd(i,siz,1){
		int x=id[i];
		dp[x]=1;
		repi(j,0,25) dp[x]+=dp[nxt[x][j]];
	}
}
void get(int k)
{
	int now=root;
	while(k){
		repi(i,0,25)if(nxt[now][i]){
			int to=nxt[now][i];
			if(dp[to]<k) k-=dp[to];
			else{
				printf("%c",i+'a'); k--;
				now=to ;break;
			}
		}
	}
	puts("");
}

7.P3975 SAM第K小子串计重复串+不计重复串
不计入重同上题,对于计重,1用对应节点的Right值替代即可,相应的选择一个字符后的转移也不是-1,而是减去转移节点的Right值。注意虽然一个节点代表一个串类,由多个串构成,但对应连边也会有多个,都会在向上传递时被选择,该类有几个串,就会被选择几次。

void get()
{
	if(dp[root]-Right[root]<k){
		puts("-1"); return ;
	}		
	int now=root;
	while(k){
		repi(i,0,25)if(nxt[now][i]){
			int to=nxt[now][i];
			if(dp[to]<k) k-=dp[to];
			else{
				printf("%c",i+'a'); k-=(type?min(k,Right[to]):1);
				now=to ;break;
			}
		}
	}
	puts("");
}

8.给出两个串,问这两个串的所有的子串中(重复出现的,只要是位置不同就算两个子串),长度大于等于k的公共子串有多少个。
对A串建SAM,排拓扑序,B串在SAM上跑,每到一个新的节点,若当前匹配长度大于=k,说明出现满足串,贡献(len-max(k,ml[fa[now]]+1)+1)*Right[now]。这样写是因为当前节点对应串类最小长度可能大于k。而对于大于k的,显然其父节点对应的串类也是满足的,因为父节点对应串类匹配长度也大于=k。因为子节点于父节点的Right集合不同,故即使从父节点而来,子节点的匹配也可以传给父节点(即两次匹配的右端点集合不同,对应子串位置是不同的。最后按拓扑序把标记更新掉即可。

ll cal(char *s)
{
	ll res=0;
	int now=root,ls=strlen(s+1),len=0;
	repi(i,1,ls){
		int c=mp[s[i]];
		if(nxt[now][c]) now=nxt[now][c],len++;
		else{
			while(now&&!nxt[now][c]) now=fa[now];
			if(!now) now=root,len=0;
			else len=ml[now]+1,now=nxt[now][c];
		}
		if(len>=k){
			res+=1ll*(len-max(k,ml[fa[now]]+1)+1)*Right[now];
			if(k<=ml[fa[now]]) mark[fa[now]]++;
		}
	}
	repd(i,siz,1){
		int x=id[i];
		res+=1ll*mark[x]*(ml[x]-max(k,ml[fa[x]]+1)+1)*Right[x];
		if(k<=ml[fa[x]]) mark[fa[x]]+=mark[x];
	}
	return res;
}

9.hdu4622 给出一个字符串,最长2000,q个询问,每次询问[l,r]区间内有多少个不同的字串。
枚举子串打表即可

struct SAM{
	int add(int c)
	{
		~~
		tot+=ml[np]-ml[fa[np]];
		return tot;
	}
}

repi(i,1,len){
			sam.init();//init时tot清零
			repi(j,i,len) ans[i][j]=sam.add(s[j]-'a');
}

10.hdu4436 给出n个数字,按字符串给出,求这n个字符串的所有子串(不重复)的和取模2012
字符串拼接,中间用10隔开,插入SAM
每个节点代表一类后缀串。那么由一个节点转移到另一个节点时,另一个节点代表新的一类后缀串,x->y 每个转移实际上只是由于添加了,一个字符导致,即x10+由哪个数字转移过去,但考虑一个点代表一类串,所加个位数要该类串数,因为该类中每个串+对应数字都能转移到y节点。同时times累加即可(先toposort,由子串继承父串)。

	int cal()
	{
		int ans=0;
		times[root]=1;
		repi(i,1,siz){
			int now=id[i];
			ans=(ans+sum[now])%mod;
			repi(j,0,9)if(nxt[now][j]&&!(!ml[now]&&!j)){//处理前导0
				int to=nxt[now][j];
				sum[to]=(sum[to]+sum[now]*10+times[now]*j)%mod;
				times[to]=(times[to]+times[now])%mod;
			}
		}
		return ans;
	}

11.CodeForces-235C 给出一个字符串s,称之为母串,然后再给出n个子串。问,对于每一个子串的所有不同的周期性的同构串在母串中出现的次数总和。
原串建SAM,查询串t在SAM上跑,因为找的串都是t的循环串,长度均为len(t)
先对t延伸1倍长,匹配长度大于k时,向上跳,直到到达一个节点,当前节点匹配长度>=k,父节点匹配长度<k,ans加上该节点的Right集合大小,并标记访问过,避免循环串出现重复的情况(以及匹配长度超过len向上跳跳到一个已访问节点的情况)

ll cal(char *s,int k,int id)//注意对这种多个查询串的标记的处理,挺长用的
{
	ll ans=0;
	int ls=2*k,now=root,len=0;
	repi(i,1,ls){
		int c=s[i]-'a';
		while(now&&!nxt[now][c]) now=fa[now],len=ml[now];
		if(!now) now=root,len=0;
		else{
			now=nxt[now][c],len++;
			if(len>=k){
				int tmp=now;
				while(vis[tmp]!=id&&ml[fa[tmp]]>=k) vis[tmp]=id,tmp=fa[tmp];
				if(vis[tmp]!=id) ans+=1ll*Right[tmp],vis[tmp]=id;
			}
		}
	}
	return ans;
}

12.给出两,字符串s1,s2,求这两个串中,都只出现一次的最短公共子串。
两个串建一个SAM,中间加分隔符区分,按拓扑序更新,每个节点额外加一个mark,维护1串2串是否都有访问这个节点(二进制看0 1两位取得0还是1),如果两个串都访问了并且Right=2(即各一次) 更新min=ml[fa[[x]]+1(因为答案要取最短的串,长的满足短的一定也存在)

int 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;
	int ans=9999;//inf
	repd(i,siz,1){
		int x=id[i];
		if(mark[x]==3&&Right[x]==2) ans=min(ans,ml[fa[x]]+1);
		mark[fa[x]]|=mark[x],Right[fa[x]]+=Right[x]; 
	} 
	return ans==9999?-1:ans;
}

~先这些,后续还应补充一些套了其它数据结构维护的题,不过还没做,留坑待填

发布了6 篇原创文章 · 获赞 1 · 访问量 100

猜你喜欢

转载自blog.csdn.net/qq_44684888/article/details/105448701
SAM