[2021.4.5多校省选模拟30]最小表示——map建边+广义SAM

前言:虽然这题前面加了个括号是“省选模拟30”,但是在accoders上是比赛“省选模拟31”里面的。

题目描述

题解

先贴出官方正解,是用的\text{hash}和后缀数组:

根据“万串皆可后缀机”的套路,这题我还是选择用后缀自动机(SAM)做。容易发现一个串的最小表示包含的信息等价于每个位置记录它前面第一个与它相同的字符出现位置的距离,比如“reverse”和“abcbadb”都可以表示为“0,0,0,2,4,0,3”(为了方便,直接记这种表示为“最小表示”)。所以可以得到一个做法,把所有子串插入广义SAM里面(自动去重),然后遍历一遍\text{parent tree}统计答案。

接下来,问题变为怎么尽可能少地插入子串,也就是让插入的串尽可能多地包含其它子串。因为一个子串相当于原串掐头去尾,而前面去掉的部分会影响后面的“最小表示”,所以我们考虑从后往前维护每一个后缀的最小表示,具体就是记录每一种字符在后面出现的第一个位置,与当前字符比较,然后更新后面的“最小表示”。

假设当前维护到第 i 个字符为 c,分两种情况:

  1. i 后面没有出现 c,那么除了 i 处的最小表示为 0 之外,其它位置不变,不会影响 i+1~n 的最小表示;
  2. i 后面出现的第一个 c 位置为 j ,那么除了 i 处最小表示为 0 、j 处最小表示为 j-i 之外,其它位置不变,不会影响 i+1~j-1 和 j+1~n 的最小表示。

由于子串倒转后统计结果不变,我们可以把最小表示倒着插入,记录每个后缀在SAM上的对应节点为las[i]

  • 那么对于第一种情况,从las[i+1]处再插入一个 0 即可;
  • 对于第二种情况,从las[j+1]处倒着插入一遍 i~j 的最小表示即可。

算法可行的关键在于第二种情况的插入次数,因为对于每一种字符,最多使其插入 n 次,所以总共插入不超过 nm 次。

但是做到这里还没完,当我们在第二种情况进行插入时,更改后的 i+1~j 的子串是不存在的,因为当且仅当子串中包含 i 时 j 的最小表示为 j-i 。怎么在统计时去掉这些子串呢?

由于\text{parent tree}上每个节点的子树存了该节点代表的子串的所有右端点位置,换过来就是插入时我们希望算入的子串的左端点位置,这时我们可以在插入时记录每个端点是否合法(比如在第二种情况中,只有 i 作为左端点时合法),统计时只统计子树中包含合法右端点的节点即可。

由于插入的是数字,故用map储存SAM上的边,总时间复杂度为O(nm\log n)

代码

评测要带freopen

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#include<cmath>
#include<vector>
#include<queue>
#include<map>
#define ll long long
#define uns unsigned
#define MAXN 50005
#define INF 0x3f3f3f3f
using namespace std;
inline ll read(){
	ll x=0;bool f=1;char s=getchar();
	while((s<'0'||s>'9')&&s>0){if(s=='-')f^=1;s=getchar();}
	while(s>='0'&&s<='9')x=(x<<1)+(x<<3)+s-'0',s=getchar();
	return f?x:-x;
}//SAM
struct SAM{
	map<int,int>ch;int len,fa;
	SAM(){len=fa=0,ch.clear();}
}sam[MAXN<<5];
int tot=1,la[MAXN],num[MAXN<<5];
inline bool fd(int id,int c,int x){
	return sam[id].ch.find(c)!=sam[id].ch.end()&&sam[id].ch[c]==x;
}
inline int samadd(int c,int las,int ad){
	int p=las,np=las=++tot;sam[np].len=sam[p].len+1;
	for(;p&&sam[p].ch[c]==0;p=sam[p].fa)sam[p].ch[c]=np;
	if(!p)sam[np].fa=1;
	else{int q=sam[p].ch[c];
		if(sam[q].len==sam[p].len+1)sam[np].fa=q;
		else{
			int nq=++tot;sam[nq]=sam[q],sam[nq].len=sam[p].len+1,sam[q].fa=sam[np].fa=nq;
			for(;p&&fd(p,c,q);p=sam[p].fa)sam[p].ch[c]=nq;
		}
	}num[np]+=ad;
	return np;
}//main
int n,m,si[15],id[MAXN];
ll ans;
char s[MAXN];
vector<int>G[MAXN<<5];
inline int dfs(int x){
	int nu=num[x];
	for(uns i=0;i<G[x].size();i++)nu+=dfs(G[x][i]);
	if(x>1&&nu>0)ans+=sam[x].len-sam[sam[x].fa].len;
	return nu;
}
signed main()
{
	freopen("string.in","r",stdin);
	freopen("string.out","w",stdout);
	n=read(),m=read();
	scanf("%s",s+1),la[n+1]=1;
	for(int i=n;i>0;i--){
		if(si[s[i]-'a']){
			int k=si[s[i]-'a'];id[k]=k-i;
			for(int j=k;j>=i;j--)la[j]=samadd(id[j],la[j+1],j==i);
		}
		else la[i]=samadd(id[i],la[i+1],1);
		si[s[i]-'a']=i;
	}
	for(int i=2;i<=tot;i++)G[sam[i].fa].push_back(i);
	dfs(1);
	printf("%lld\n",ans);
	return 0;
}

由于多数人用的是没被卡掉的空间小的\text{hash},我的空间显得格外突兀:

猜你喜欢

转载自blog.csdn.net/weixin_43960287/article/details/115443431