初探后缀自动机

本篇旨在讲解部分常见的 SAM 技巧,以及经典的 SAM 题目。

如果不会 SAM 模板(这一篇有些内容现在看起来比较 naive,后面也许会修)

哎还是现代的好

几点暴论:

  • 如果题目中求的是什么子串的出现次数,那直接无脑上 SAM。因为 SAM 的 parent 树是反串的后缀树,求出现次数时,二者并无区别。
  • 如果题目中涉及了「前缀」「后缀」等字样,请仔细品味在使用 SAM 时是否应该对反串建 parent 树。

废案:

Luogu P3804 【模板】后缀自动机(SAM)#

我们发现 SAM 几乎可以当作子串的 AC 自动机。

将 SAM 上的每个前缀节点的权值置为 1。那么类似地,对于 SAM 上的一个节点,统计其在 link 树上的子树和。那么,该等价类中的每个字符串,出现次数均为该子树和。

原因如下:根据 endpos 的定义,SAM 上所有前缀节点互不相同。同时,该前缀必然作为其所在等价类中的最长串。因此,不会出现某个该等价类的字符串没出现次数的情况(最长串都出现了,更短的串必然出现)。

# include <bits/stdc++.h>

const int N=2000010;

struct Node{
	int ch[26],link,len;
}sam[N];
int cnt=1;
char s[N];
int n;
int siz[N];
std::vector <int> G[N];

long long ans;

inline int sam_extend(int last,int c){
	int cur=++cnt,p=last;
	sam[cur].len=sam[last].len+1;
	siz[cur]=1;
	while(!sam[p].ch[c]) sam[p].ch[c]=cur,p=sam[p].link;
	if(!p){
		sam[cur].link=1;
		return cur;
	}
	int q=sam[p].ch[c];
	if(sam[p].len+1==sam[q].len){
		sam[cur].link=q;
		return cur;
	}
	int clone=++cnt;
	sam[clone]=sam[q],sam[clone].len=sam[p].len+1;
	while(p&&sam[p].ch[c]==q) sam[p].ch[c]=clone,p=sam[p].link;
	sam[cur].link=sam[q].link=clone;
	return cur;
}

void dfs(int i){
	for(auto v:G[i]) dfs(v),siz[i]+=siz[v];
	if(siz[i]>1) ans=std::max(ans,1ll*siz[i]*sam[i].len);
	return;
}

int main(void){
	scanf("%s",s+1);
	n=strlen(s+1);
	int last=1;
	for(int i=1;i<=n;++i){
		last=sam_extend(last,s[i]-'a');
	}
	for(int i=2;i<=cnt;++i) G[sam[i].link].push_back(i);
	dfs(1);
	printf("%lld",ans);
	return 0;
}

Luogu P4070 [SDOI2016] 生成魔咒#

即,需要 SAM 支持:末端插入字符,在线询问本质不同子串数。

考察哪些串是新的本质不同子串。事实上,只有在 i 这个位置第一次出现的串才是新的本质不同子串。那么答案就呼之欲出了:找到 s[1,i] 的前缀节点,该等价类中的节点数量即为答案。

# include <bits/stdc++.h>

const int N=100010,INF=0x3f3f3f3f;

struct Node{
	int len,link;
	std::map <int,int> nex;
}sam[N*2];

char s[N];
int n;
int last=1;
int tot=1;
long long ans;

inline int read(void){
	int res,f=1;
	char c;
	while((c=getchar())<'0'||c>'9')
		if(c=='-')f=-1;
	res=c-48;
	while((c=getchar())>='0'&&c<='9')
		res=res*10+c-48;
	return res*f;
}
inline void sam_extend(int c){
	int now=++tot,p=last;
	sam[now].len=sam[last].len+1;
	last=now;
	while(p&&(!sam[p].nex[c]))
		sam[p].nex[c]=now,p=sam[p].link;
	if(!p){
		sam[now].link=1;
		return;
	}
	int qnode=sam[p].nex[c];
	if(sam[p].len+1==sam[qnode].len){
		sam[now].link=qnode;
		return;
	}
	int clone=++tot;
	sam[clone]=sam[qnode];
	sam[clone].len=sam[p].len+1;
	sam[qnode].link=clone,sam[now].link=clone;
	while(p&&sam[p].nex[c]==qnode){
		sam[p].nex[c]=clone,p=sam[p].link;
	}
	return;
}
int main(void){
	n=read();
	for(int i=1;i<=n;++i){
		int x=read();
		sam_extend(x);
		ans+=sam[last].len-sam[sam[last].link].len;
		printf("%lld\n",ans);
	}
	return 0;
}

Luogu P3975 [TJOI2015] 弦论#

考虑利用 SAM 的 DAG 结构解决。注意到性质:SAM 上从起始节点开始沿 DAG 行走,路径形成的字符串必然是字符串的子串,且所有子串都可以被某条路径形成,且不会有不同路径形成相同字符串。

据此可以贪心。设 f(i) 表示,从 i 号节点出发,可以走出的字符串数量。另记 g(i) 表示从 i 号节点出发,可以走出的空字符串数量(即 i 走到 i)。

现在考虑从起始节点开始行走,设当前位于 i。若 kg(i),则字符串就是当前路径。否则,从小大大枚举出边 (i,j),并判断:

  • kf(j),则路径必然经过边 (i,j),记录下 (i,j) 上的字符,停止枚举,并将 i 移动到 j
  • k>f(j),则经过 (i,j) 的数量不足 k,因此将 k 减去 f(j)

T=0 时,只有本质不同子串算作不同。那么有 g(i)=1f(i) 可以拓扑排序得出。

T=1 时,位置不同子串算不同,那么有 g(i) 等于 i 的 endpos 集合大小,可以 link 树上 DP 得出。

# include <bits/stdc++.h>

const int N=1000010,INF=0x3f3f3f3f;

struct Node{
	int link,len,nex[26];
}sam[N];
int tot=1,last=1;
int n,T,k;
int esize[N]; // 文中的 g 数组
char s[N];
long long f[N];
std::vector <int> G[N];

inline int read(void){
	int res,f=1;
	char c;
	while((c=getchar())<'0'||c>'9')
		if(c=='-')f=-1;
	res=c-48;
	while((c=getchar())>='0'&&c<='9')
		res=res*10+c-48;
	return res*f;
}
inline void sam_extend(int c){
	int now=++tot,p=last;
	esize[now]=1;
	sam[now].len=sam[last].len+1,last=now;
	while(p&&!sam[p].nex[c]){
		sam[p].nex[c]=now,p=sam[p].link;
	}
	if(!p){
		sam[now].link=1;
		return;
	}
	int qnode=sam[p].nex[c];
	if(sam[qnode].len==sam[p].len+1){
		sam[now].link=qnode;
		return;
	}
	int clone=++tot;
	sam[clone]=sam[qnode],sam[clone].len=sam[p].len+1,sam[qnode].link=sam[now].link=clone;
	while(p&&sam[p].nex[c]==qnode) sam[p].nex[c]=clone,p=sam[p].link;
	return;
}
void dfs_esize(int i){
	for(auto to:G[i]){
		dfs_esize(to),esize[i]+=esize[to];
	}
	return;
}
long long dfs(int i){
	if(f[i]) return f[i];
	f[i]=esize[i];
	for(int j=0;j<26;++j){
		int to=sam[i].nex[j];
		if(to)  f[i]+=dfs(to);
	}
	return f[i];
}
void solve(int i,int resk){
	if(resk<=esize[i]) return;
	resk-=esize[i];
	for(int j=0;j<26;++j){
		int to=sam[i].nex[j];
		if(!to) continue;
		if(resk>f[to]){
			resk-=f[to];
			continue;
		}
		putchar('a'+j),solve(to,resk);
		return;
	}
	return;
}
int main(void){
	scanf("%s",s+1);
	n=strlen(s+1),T=read(),k=read();
	for(int i=1;i<=n;++i) sam_extend(s[i]-'a');
	for(int i=2;i<=tot;++i) G[sam[i].link].push_back(i);
	if(T) dfs_esize(1);
	else
		for(int i=1;i<=tot;++i) esize[i]=1;
	esize[1]=0,dfs(1);
	if(k>f[1]) printf("-1");
	else{
		solve(1,k);
	}
	return 0;
}

Luogu P4248 [AHOI2013] 差异#

直接对正串做是较为困难的,因为 SAM 构建的整个过程都没有刻画过后缀节点。

考虑将串取反,所求即为所有前缀节点对的在 link 树上的距离之和。可以 DFS link 树解决。

# include <bits/stdc++.h>

const int N=500010,INF=0x3f3f3f3f;

inline int read(void){
	int res,f=1;
	char c;
	while((c=getchar())<'0'||c>'9')
		if(c=='-') f=-1;
	res=c-48;
	while((c=getchar())>='0'&&c<='9')
		res=res*10+c-48;
	return res*f;
}
char s[N];
int n;

struct Node{
	int link,nex[26],len;
}sam[N*2];

int last=1,cnt=1;
int siz[N*2];

inline void extend(int c){
	int cur=++cnt,p=last,q;
	sam[cur].len=sam[p].len+1,last=cur,siz[cur]=1;
	while(p&&!sam[p].nex[c]) sam[p].nex[c]=cur,p=sam[p].link;
	if(!p) return sam[cur].link=1,void();
	q=sam[p].nex[c];
	if(sam[p].len+1==sam[q].len) return sam[cur].link=q,void();
	int clone=++cnt;
	sam[clone]=sam[q],sam[clone].len=sam[p].len+1,sam[cur].link=sam[q].link=clone;
	while(p&&sam[p].nex[c]==q) sam[p].nex[c]=clone,p=sam[p].link;
	return;
}
long long ans;
std::vector <int> G[N*2];

void dfs(int x){
	for(auto y:G[x]) dfs(y),siz[x]+=siz[y],ans+=1ll*(sam[y].len-sam[x].len)*(n-siz[y])*siz[y];
	return;
}

inline void init(void){
	for(int i=2;i<=cnt;++i){
		G[sam[i].link].push_back(i);
	}
	dfs(1);
	return;
}

int main(void){
	scanf("%s",s+1),n=strlen(s+1);
	for(int i=n;i;--i) extend(s[i]-'a');
	init();
	printf("%lld",ans);

	return 0;
}

Codeforces 653F Paper task#

建出 SAM,考虑对于某个等价类一起处理。

对于某个等价类,找出其任意一 endpos,设该等价类中的字符串左端点 s 位于 [l,r],右端点为 t。那么合法括号串应当满足后缀和非负(将左括号看作 1,右括号看作 1),区间和为 0 两个条件。注意到后缀和非负相当于给左端点提出了下界限制,这可以二分 + ST 表求出。区间和为 0 可以转后缀和后变为端点 s 处和端点 t+1 处后缀相等。将同一后缀和的端点放入 vector,lower_bound 和 upper_bound 即可求出合法 s 端点数量。

# include <bits/stdc++.h>

const int N=1000010,INF=0x3f3f3f3f;

inline int read(void){
	int res,f=1;
	char c;
	while((c=getchar())<'0'||c>'9')
		if(c=='-') f=-1;
	res=c-48;
	while((c=getchar())>='0'&&c<='9')
		res=res*10+c-48;
	return res*f;
}

struct Node{
	int link,len,nex[2],edp;
}sam[N];
int n;
char s[N];
int cnt=1,last=1;

inline void extend(int c,int i){
	int cur=++cnt,p=last;
	sam[cur].len=sam[last].len+1,last=cur,sam[cur].edp=i;
	while(p&&!sam[p].nex[c]) sam[p].nex[c]=cur,p=sam[p].link;
	if(!p) return sam[cur].link=1,void();
	int q=sam[p].nex[c];
	if(sam[p].len+1==sam[q].len) return sam[cur].link=q,void();
	int clone=++cnt;
	sam[clone]=sam[q],sam[clone].len=sam[p].len+1,sam[cur].link=sam[q].link=clone;
	while(p&&sam[p].nex[c]==q) sam[p].nex[c]=clone,p=sam[p].link;
	return;
}
std::vector <int> G[N];
void dfs(int x){
	for(auto y:G[x]) dfs(y),sam[x].edp=sam[x].edp?sam[x].edp:sam[y].edp;
	return;
}

int S[N];
int mx[N][20];

inline void st_init(void){
	for(int i=1;i<=n;++i) mx[i][0]=S[i];
	for(int j=1;(1<<j)<=n;++j)
		for(int i=1;i+(1<<j)-1<=n;++i) mx[i][j]=std::min(mx[i][j-1],mx[i+(1<<(j-1))][j-1]);	
	return;
}
inline int qu(int l,int r){
	int k=std::__lg(r-l+1);
	return std::min(mx[l][k],mx[r-(1<<k)+1][k]);
}

inline int getmin(int l,int r,int x){
	int ans=0;
	while(l<=r){
		int mid=(l+r)>>1;
		if(qu(mid,x)<S[x+1]) ans=mid,l=mid+1;
		else r=mid-1;
	}
	return ans;
}
std::unordered_map <int,std::vector <int> > vec;

inline int getans(int l,int r,int x){
	if(l>r) return 0;
	auto tl=std::lower_bound(vec[x].begin(),vec[x].end(),l);
	auto tr=std::upper_bound(vec[x].begin(),vec[x].end(),r);
	return tr-tl;
}

int main(void){
//	freopen("in.txt","r",stdin);
	
	n=read();
	scanf("%s",s+1);
	for(int i=1;i<=n;++i) extend(s[i]-'(',i);
	for(int i=n;i;--i) S[i]=S[i+1]+((s[i]=='(')?-1:1);
	for(int i=1;i<=n;++i) vec[S[i]].push_back(i);
	for(int i=2;i<=cnt;++i) G[sam[i].link].push_back(i);
	dfs(1);
	
	st_init();
	long long ans=0;
	
	for(int i=2;i<=cnt;++i){
		int pos=sam[i].edp,lenl=sam[sam[i].link].len+1,lenr=sam[i].len;		
		int l=getmin(1,pos,pos)+1;
		ans+=getans(std::max(l,pos-lenr+1),pos-lenl+1,S[pos+1]);
	}
	
	printf("%lld",ans);

	return 0;
}

LOJ6071「2017 山东一轮集训 Day5」字符串#

贪心一定不劣。对于一个串 t,如果我们能够在某个 si 中匹配,那么一定不会提前转入 si+1

si 建出 SAM。完善其 DAG 结构,具体地,对于 si 的 SAM 中某个节点的出边 tri(x,c),若其不存在则令其转到最小的 j 使得 sj 中存在字符 c。最后在 DAG 上 DP 即可。

SP1812 LCS2 - Longest Common Substring II#

并不需要建出广义 SAM。考虑对其中一个串建出 SAM,并像 AC 自动机上匹配一样,将其它串放在 SAM 上遍历,维护其它所有串在 SAM 上的匹配长度。每个节点的最终匹配长度即为该节点在每个串上的匹配长度的最小值。

具体地,对于每个其它串 T,匹配过程如下:

  • 维护变量 len,cur,表示 SAM 上匹配的长度。初始值为 len=0,cur=1
  • 遍历 i=1,2,,|T|,检查 tr(cur,Ti) 是否存在。
    • 若存在,则令 len 增加 1cur 转到 tr(cur,Ti)
    • 若不存在,则遍历 cur 的后缀链接,直到存在 Ti 的转移边,或到达根为止。若 tr(cur,Ti) 存在,则将 len 置为 cur 的节点长度加 1,将 cur 转到 tr(cur,Ti)。否则,令 len0,cur1

考虑到 n10,因此这种做法是可以通过的。另外,如果我们选取最短的串建立 SAM,设其串长为 l,那么复杂度为 O(L|Σ|+lLl)=O(L|Σ|),因此 n 更大时该做法仍然成立。

# include <bits/stdc++.h>

const int N=200010,INF=0x3f3f3f3f;
struct Node{
	int len,link,nex[26];
}sam[N];
int tot=1,last=1;
char s[N];
int mx[N],himx[N],n;
int cnt[N],od[N];

inline int read(void){
	int res,f=1;
	char c;
	while((c=getchar())<'0'||c>'9')
		if(c=='-')f=-1;
	res=c-48;
	while((c=getchar())>='0'&&c<='9')
		res=res*10+c-48;
	return res*f;
}
inline void sam_extend(int c){
	int now=++tot,p=last;
	sam[now].len=sam[last].len+1,last=now;
	while(p&&!sam[p].nex[c]) sam[p].nex[c]=now,p=sam[p].link;
	if(!p){
		sam[now].link=1;
		return;
	}
	int qnode=sam[p].nex[c];
	if(sam[p].len+1==sam[qnode].len){
		sam[now].link=qnode;
		return;
	}else{
		int clone=++tot;
		sam[clone]=sam[qnode],sam[clone].len=sam[p].len+1,sam[now].link=sam[qnode].link=clone;
		while(p&&sam[p].nex[c]==qnode) sam[p].nex[c]=clone,p=sam[p].link;
	}
	return;
}
inline void chkmax(int &x,int v){
	x=std::max(x,v);
	return;
}
int main(void){
	scanf("%s",s+1);
	n=strlen(s+1);
	for(int i=1;i<=n;++i) sam_extend(s[i]-'a');
	for(int i=1;i<=tot;++i) himx[i]=sam[i].len;
	for(int i=1;i<=tot;++i) ++cnt[sam[i].len];
	for(int i=1;i<=tot;++i) cnt[i]+=cnt[i-1];
	for(int i=1;i<=tot;++i) od[cnt[sam[i].len]--]=i;

	while(~scanf("%s",s+1)){
		n=strlen(s+1);
		memset(mx,0,sizeof(mx));
		int len=0,cur=1;
		for(int i=1;i<=n;++i){
			int c=s[i]-'a';
			if(sam[cur].nex[c]) cur=sam[cur].nex[c],++len,chkmax(mx[cur],len);
			else{
				while(cur&&!sam[cur].nex[c]) cur=sam[cur].link;
				if(!cur) len=0,cur=1;
				else len=sam[cur].len+1,cur=sam[cur].nex[c],chkmax(mx[cur],len);
			}
		}
		for(int i=tot;i;--i) chkmax(mx[sam[od[i]].link],mx[od[i]]);
		for(int i=1;i<=tot;++i) himx[i]=std::min(himx[i],mx[i]);
	}
	int ans=0;
	for(int i=1;i<=tot;++i) chkmax(ans,himx[i]);
	printf("%d",ans); 
	return 0;
}

Codeforces 235C Cyclical Quest#

对原串 S 建出 SAM,考虑每个询问串 T。对于循环移位,我们有经典的处理方法:将 rev(T) 拼接到 T 之后,那么对于每个 |T|<i2|T|[i|T|+1,i] 就是一个循环移位。

采用上一题类似的匹配方法。但本题中,我们任何时刻都不应当让匹配长度超过 |T|,同时两个本质相同的循环移位不应当被计算多次。因此匹配流程如下:

  • 维护变量 len,cur,表示 SAM 上匹配的长度。初始值为 len=0,cur=1

  • 遍历 i=1,2,,2|T|,并执行:

    1. 检查 tr(cur,Ti) 是否存在。

      若存在,则令 len 增加 1cur 转到 tr(cur,Ti)

      若不存在,则遍历 cur 的后缀链接,直到存在 Ti 的转移边,或到达根为止。若 tr(cur,Ti) 存在,则将 len 置为 cur 的节点长度加 1,将 cur 转到 tr(cur,Ti)。否则,令 len0,cur1

    2. 检查匹配长度 len 是否大于 |T|。若是,则将 len 减小 1,若此行为使得 len 不再属于 cur 的长度区间,则 cur 变为 link(cur)

    3. i>|T|,检查匹配长度 len 是否恰好为 |T|,且 cur 节点没有被打上标记 T。若是,则答案累加上 cur 节点的 endpos 集合大小,并给 cur 节点打上标记 T,防止被重复计算。

# include <bits/stdc++.h>

const int N=2000010,INF=0x3f3f3f3f;

inline int read(void){
	int res,f=1;
	char c;
	while((c=getchar())<'0'||c>'9')
		if(c=='-') f=-1;
	res=c-48;
	while((c=getchar())>='0'&&c<='9')
		res=res*10+c-48;
	return res*f;
}

struct Node{
	int link,len,nex[26],edp;
}sam[N];
int n;
char s[N];
int cnt=1,last=1;
int siz[N];

inline void extend(int c){
	int cur=++cnt,p=last;
	sam[cur].len=sam[last].len+1,last=cur,siz[cur]=1;
	while(p&&!sam[p].nex[c]) sam[p].nex[c]=cur,p=sam[p].link;
	if(!p) return sam[cur].link=1,void();
	int q=sam[p].nex[c];
	if(sam[p].len+1==sam[q].len) return sam[cur].link=q,void();
	int clone=++cnt;
	sam[clone]=sam[q],sam[clone].len=sam[p].len+1,sam[cur].link=sam[q].link=clone;
	while(p&&sam[p].nex[c]==q) sam[p].nex[c]=clone,p=sam[p].link;
	return;
}
std::vector <int> G[N];
void dfs(int x){
	for(auto y:G[x]) dfs(y),siz[x]+=siz[y];
	return;
}
int tag[N];

int main(void){
	scanf("%s",s+1),n=strlen(s+1);
	for(int i=1;i<=n;++i) extend(s[i]-'a');
	for(int i=2;i<=cnt;++i) G[sam[i].link].push_back(i);
	dfs(1);
	memset(tag,-1,sizeof(tag));
	
	int T=read();
	while(T--){
		scanf("%s",s+1);
		int m=strlen(s+1);
		int len=0,cur=1;
		long long ans=0;
		
		for(int i=1;i<=2*m;++i){
			int c=(i<=m)?s[i]-'a':s[i-m]-'a';
			if(sam[cur].nex[c]) cur=sam[cur].nex[c],++len;
			else{
				while(cur&&!sam[cur].nex[c]) cur=sam[cur].link;
				if(!cur) len=0,cur=1;
				else len=sam[cur].len+1,cur=sam[cur].nex[c];
			}
			if(len>m&&((--len)==sam[sam[cur].link].len)) cur=sam[cur].link;			
			if(i<=m||tag[cur]==T||len!=m) continue;
			tag[cur]=T;
			ans+=siz[cur];
		}
		printf("%lld\n",ans);	
	}
	return 0;
}

P6640 [BJOI2020] 封印#

f(i) 表示 s[i,n]t 的 LCP 长度,使用 SAM 或 SA 容易求出。

考虑答案为

maxi=lr{min(f(i),ri+1)}

使用二分答案规避掉内层的 min。具体地,设当前二分的答案为 x,那么我们只检查 ri+1x 的部分中 f(i) 的值是否大于等于 x

P4770 [NOI2018] 你的名字#

转化为求 T 的多少本质不同子串在 S[l,r] 中出现过。将 T 放在 S 的 SAM 上匹配,记录下每个位置 i 的最长匹配后缀长度 ansi。匹配方法如下:

  • 维护变量 len,cur,表示 SAM 上匹配的长度。初始值为 len=0,cur=1

  • 遍历 i=1,2,,|T|,执行:

    1. mlen 表示 s[l,r] 中出现过最长的在 tr(cur,Ti) 等价类中的字符串长度,若不存在则为 0

    2. mlen 不为 0,则令 cur 转到 tr(cur,Ti)len 变为 min(len+1,mlen),然后执行 3。否则,该等价类中字符串未出现,若 cur 为起始节点,则令 len0,转到 3。否则令 cur 转到 link(cur),然后转到 1。

    3. ansilen

随后建出 T 的 SAM,在每个前缀 i 对应节点处打上标记 ansi。则节点 x 对应等价类的最长匹配长度即子树标记的最大值对该节点长度取 min 后的结果,据此容易计算出每个节点的贡献。

重点在于求 mlen,即 S 的 SAM 上某个节点的等价类中字符串在 s[l,r] 上出现过的最大长度。考虑使用可持久化线段树合并维护出每个节点的 endpos 集合。若该节点 endpos 集合中,值在 [l,r] 中的元素的最大值为 x,该节点长度为 y,那么出现过的最大长度即 min(y,xl+1)

# include <bits/stdc++.h>

const int N=1000010,INF=0x3f3f3f3f;

struct Node{
	int ch[26],link,len;
	inline void clear(void){
		memset(ch,0,sizeof(ch));
		link=len=0;
		return;
	}
};

char s[N],t[N];

int n,m,L,R;

int rt[N];
int ans[N];

namespace sgt{
	const int MN=N*40;
	int lc[MN],rc[MN],cnt;
	void change(int &k,int l,int r,int x){
		if(!k) k=++cnt;
		if(l==r) return;
		int mid=(l+r)>>1;
		if(x<=mid) change(lc[k],l,mid,x);
		else change(rc[k],mid+1,r,x);
		return;
	}
	int query(int &k,int l,int r,int rp){
		if(!k||l>rp) return 0;
		if(l==r) return l;
		int mid=(l+r)>>1,qr;
		if(qr=query(rc[k],mid+1,r,rp)) return qr;
		return query(lc[k],l,mid,rp);
	}
	void merge(int &cur,int x,int y,int l,int r){
		if(!x||!y) return cur=x|y,void();
		cur=++cnt;
		if(l==r) return;
		int mid=(l+r)>>1;
		merge(lc[cur],lc[x],lc[y],l,mid),merge(rc[cur],rc[x],rc[y],mid+1,r);
		return;
	}
}

namespace ssam{
	Node sam[N];
	int cnt,lst;
	inline void init(void){
		cnt=1,lst=1;
		return;
	}
	inline void extend(int c){
		int cur=++cnt,p=lst,q,clone;
		sam[cur].len=sam[lst].len+1,lst=cur;
		while(p&&!sam[p].ch[c]) sam[p].ch[c]=cur,p=sam[p].link;
		if(!p) return sam[cur].link=1,void();
		q=sam[p].ch[c];
		if(sam[p].len+1==sam[q].len) return sam[cur].link=q,void();
		clone=++cnt,sam[clone]=sam[q],sam[clone].len=sam[p].len+1;
		sam[q].link=sam[cur].link=clone;
		while(p&&sam[p].ch[c]==q) sam[p].ch[c]=clone,p=sam[p].link;
		return;
	}
	int sum[N],p[N];
	inline void solve(void){
		for(int i=1;i<=n;++i) extend(s[i]-'a'),sgt::change(rt[lst],1,n,i);
		for(int i=1;i<=cnt;++i) ++sum[sam[i].len];
		for(int i=1;i<=n;++i) sum[i]+=sum[i-1];
		for(int i=1;i<=cnt;++i) p[sum[sam[i].len]--]=i;
		for(int i=cnt;i;--i) if(p[i]!=1) sgt::merge(rt[sam[p[i]].link],rt[sam[p[i]].link],rt[p[i]],1,n);
		return;
	}
	inline int qlen(int x){
		if(!x) return 0;
		int res=sgt::query(rt[x],1,n,R);
		if(res<L+sam[sam[x].link].len) return 0;
		return std::min(sam[x].len,res-L+1);
	}
	inline void matcht(void){
		int p=1,c,v;
		for(int i=1;i<=m;++i){
			c=t[i]-'a',v=std::min(sam[p].len+1,qlen(sam[p].ch[c]));
			while(p&&!v) p=sam[p].link,v=std::min(sam[p].len+1,qlen(sam[p].ch[c]));
			ans[i]=std::min(ans[i-1]+1,v);
			if(p) p=sam[p].ch[c];
			else p=1;
		}
		return;
	}
}
namespace tsam{
	Node sam[N];
	int cnt,lst;
	int mx[N];
	inline void init(void){
		cnt=1,lst=1,sam[1].clear(),mx[1]=0;
		return;
	}
	inline int newnode(void){
		++cnt,sam[cnt].clear(),mx[cnt]=0;
		return cnt;
	}
	inline void extend(int c){
		int cur=newnode(),p=lst,q,clone;
		sam[cur].len=sam[lst].len+1,lst=cur;
		while(p&&!sam[p].ch[c]) sam[p].ch[c]=cur,p=sam[p].link;
		if(!p) return sam[cur].link=1,void();
		q=sam[p].ch[c];
		if(sam[p].len+1==sam[q].len) return sam[cur].link=q,void();
		clone=newnode(),sam[clone]=sam[q],sam[clone].len=sam[p].len+1;
		sam[q].link=sam[cur].link=clone;
		while(p&&sam[p].ch[c]==q) sam[p].ch[c]=clone,p=sam[p].link;
		return;
	}
	int sum[N],p[N];
	inline void solve(void){
		for(int i=1;i<=m;++i) extend(t[i]-'a'),mx[lst]=ans[i];
		for(int i=0;i<=m;++i) sum[i]=0;
		for(int i=1;i<=cnt;++i) ++sum[sam[i].len];
		for(int i=1;i<=m;++i) sum[i]+=sum[i-1];
		for(int i=1;i<=cnt;++i) p[sum[sam[i].len]--]=i;
		for(int i=cnt;i;--i) if(p[i]!=1) mx[sam[p[i]].link]=std::max(mx[sam[p[i]].link],mx[p[i]]);
		long long ans=0;
		for(int i=2;i<=cnt;++i){
			int ml=sam[sam[i].link].len+1,mr=std::min(sam[i].len,mx[i]);
			ans-=std::max(0,mr-ml+1);
			ans+=sam[i].len-sam[sam[i].link].len;
		}
		printf("%lld\n",ans);
		return;
	}	
}

inline int read(void){
	int res,f=1;
	char c;
	while((c=getchar())<'0'||c>'9')
		if(c=='-') f=-1;
	res=c-48;
	while((c=getchar())>='0'&&c<='9')
		res=res*10+c-48;
	return res*f;
}

int main(void){
	scanf("%s",s+1),n=strlen(s+1);
	ssam::init(),ssam::solve();
	int q=read();
	while(q--){
		scanf("%s",t+1),m=strlen(t+1);
		L=read(),R=read();
		ssam::matcht(),tsam::init(),tsam::solve();
	}
	
	return 0;
} 

Codeforces 1037H Security#

考虑贪心构造答案。若 S1[i]=T[i] 可行,且递归进 i+1 后有解,则令 S1[i]=T[i]。否则找到最小的 c>T[i] 使得 Si[i]=c 可行,此时 S1[1i] 就是答案。

S1 需要是 S[l,r] 的子串,可以使用上一题类似的线段树合并方法来判断可行性。

# include <bits/stdc++.h>

const int N=500010,INF=0x3f3f3f3f;

struct Node{
	int ch[26],link,len;
	inline void clear(void){
		memset(ch,0,sizeof(ch));
		link=len=0;
		return;
	}
};

char s[N],t[N],ret[N];
int cc;

int n,m;

int rt[N];

namespace sgt{
	const int MN=N*40;
	int lc[MN],rc[MN],cnt;
	void change(int &k,int l,int r,int x){
		if(!k) k=++cnt;
		if(l==r) return;
		int mid=(l+r)>>1;
		if(x<=mid) change(lc[k],l,mid,x);
		else change(rc[k],mid+1,r,x);
		return;
	}
	int query(int &k,int l,int r,int rp){
		if(!k||l>rp) return 0;
		if(l==r) return l;
		int mid=(l+r)>>1,qr;
		if(qr=query(rc[k],mid+1,r,rp)) return qr;
		return query(lc[k],l,mid,rp);
	}
	void merge(int &cur,int x,int y,int l,int r){
		if(!x||!y) return cur=x|y,void();
		cur=++cnt;
		if(l==r) return;
		int mid=(l+r)>>1;
		merge(lc[cur],lc[x],lc[y],l,mid),merge(rc[cur],rc[x],rc[y],mid+1,r);
		return;
	}
}

namespace ssam{
	Node sam[N];
	int cnt,lst;
	inline void init(void){
		cnt=1,lst=1;
		return;
	}
	inline void extend(int c){
		int cur=++cnt,p=lst,q,clone;
		sam[cur].len=sam[lst].len+1,lst=cur;
		while(p&&!sam[p].ch[c]) sam[p].ch[c]=cur,p=sam[p].link;
		if(!p) return sam[cur].link=1,void();
		q=sam[p].ch[c];
		if(sam[p].len+1==sam[q].len) return sam[cur].link=q,void();
		clone=++cnt,sam[clone]=sam[q],sam[clone].len=sam[p].len+1;
		sam[q].link=sam[cur].link=clone;
		while(p&&sam[p].ch[c]==q) sam[p].ch[c]=clone,p=sam[p].link;
		return;
	}
	int sum[N],p[N];
	inline void solve(void){
		for(int i=1;i<=n;++i) extend(s[i]-'a'),sgt::change(rt[lst],1,n,i);
		for(int i=1;i<=cnt;++i) ++sum[sam[i].len];
		for(int i=1;i<=n;++i) sum[i]+=sum[i-1];
		for(int i=1;i<=cnt;++i) p[sum[sam[i].len]--]=i;
		for(int i=cnt;i;--i) if(p[i]!=1) sgt::merge(rt[sam[p[i]].link],rt[sam[p[i]].link],rt[p[i]],1,n);
		return;
	}
	inline bool chk(int x,int l,int r){
		if(!x) return 0;
		return sgt::query(rt[x],1,n,r)>=l;
	}
}
using namespace ssam;

inline int read(void){
	int res,f=1;
	char c;
	while((c=getchar())<'0'||c>'9')
		if(c=='-') f=-1;
	res=c-48;
	while((c=getchar())>='0'&&c<='9')
		res=res*10+c-48;
	return res*f;
}

inline bool calc(int l,int r,int x,int cur){
	int c=t[x]-'a';
	if(c<0) c=-1;
	if(c>=0&&chk(sam[cur].ch[c],l+x-1,r)&&calc(l,r,x+1,sam[cur].ch[c])) // 判断是否出现 下一位是否合法
		return ret[++cc]=t[x],true;
	for(int cp=c+1;cp<26;++cp) if(sam[cur].ch[cp]&&chk(sam[cur].ch[cp],l+x-1,r))
		return ret[++cc]=cp+'a',true;
	return false;
}

int main(void){
	scanf("%s",s+1),n=strlen(s+1),ssam::init(),ssam::solve();
	int q=read();
	while(q--){
		int l=read(),r=read();
		scanf("%s",t+1),m=strlen(t+1),cc=0;
		if(calc(l,r,1,1)){
			for(int i=1;i<=cc;++i) putchar(ret[cc-i+1]);
			puts("");
		}else puts("-1");
	}
	
	return 0;
}

Codeforces 1276F Asterisk Substrings#

注意到答案形如:s,s,t,st,

考虑分类讨论。对于 ,贡献为 1。对于 s 型的答案,当且仅当 s 是原串 S 的子串时合法,容易建 SAM 求出;对于 s 型的答案,则要求 s 是原串前缀 [1,|S|1] 的子串,对于 t 型的答案,要求 t 是原串后缀 [2,|S|] 的子串,这些可以分别在原串和反串上建 SAM 求出。

现在考虑形如 st 的串。s 需要是 S 的子串,因此考虑 S 的子串构成的等价类,等价类中的每个字符串,可以选择的 t 数量是相同的。设该等价类的 endpos 集合为 E,那么对于任意 eE,后缀 S[e+2,|S|] 中的每个前缀都可以作为合法的 t。将反串 SAM 上的前缀节点 |S|(e+2)+1 到根的路径打上标记,那么所有被标记的边长之和即为答案。

可以使用线段树合并的方法维护每个等价类被打上标记节点的边长之和。对于每个等价类,我们将被打上标记的节点按照 DFS 序顺序记录在线段树中。根据经典结论,合并时,该子树内的答案等于左子树合并后的答案,加上右子树合并后的答案,再减去左子树中最靠右的节点和右子树最靠左的节点的 LCA 深度。

P4218 [CTSC2010] 珠宝商#

  • 朴素暴力

    枚举每个节点作为根。统计从该节点开始的路径数量,从根往下 DFS,维护根到当前节点形成的字符串在 SAM 上对应的节点 cur,每到一个点,答案累加上 cur 节点的 endpos 集合大小。若某一时刻该字符串不再在 SAM 中,则返回。时间复杂度 O(n2)

  • 另类暴力

    枚举每个节点 r 作为根。统计经过该节点的路径数量,考虑路径 srt,拆成两部分 srrt。设 f(p) 表示有多少 sr 的字符串在特征串 E 中在位置 p 结束,g(p) 表示有多少 rt 的字符串在特征串 E 中在位置 p 开始。

    g(p) 可以通过反转 E 后,在 f(p) 中类似的算法求出。现在只需要考虑求出 f

    对于所有路径 sr,如果它在 SAM 上匹配后位于的节点 cur,那么我们将 cur 节点的权值加上 1。最后自上往下遍历 fail 树,将儿子的权值加上父亲的权值即可。最后对于每个 p,取出 s[1,p] 对应节点的权值,即为 f(p)

    因此现在重点在于求出 cur。设有边 (u,v),其中 uv 的父亲。ur 的字符串为 S,在 SAM 上匹配的节点为 cur,那么字符串会从 S 变为 S=cv+S。考察 cur 的变化:

    • S 不为 cur 等价类中的最长串,则检查等价类中长度为 |S| 的字符串是否恰为 S。若是,则 cur 不变,否则 S 没有出现在 E 中,返回。随意求得 cur 的某个 endpos R(cur),则只需要检查是否有 cv=E[R(cur)|S|+1]

    • Scur 等价类中的最长串,则 cv+S 所属等价类只能为 cur 在 fail 树上的某个儿子。检查 cur 在 fail 树上是否存在儿子 nex,使得 cv=E[R(nex)len(cur)]。这样的儿子可以预处理得出。

      若存在,则 cur 转到 nex。否则 S 没有出现在 E 中,返回。

    最后,注意到贡献可以来源于同一子树。贡献可删除,因此要再 DFS 某个子树,删除子树的相互贡献。

    该暴力的时间复杂度为 O(n|E|)

考虑点分治结合另类暴力。当子树大小不超过 |E| 时,采用朴素暴力,否则采用另类暴力。则时间复杂度为 O(nlogn|E|)

SP687 REPEATS - Repeats#

注意到 border 和周期是对应的,而周期和整周期有很强的联系。

建立 SAM。对于每个等价类,设该 endpos 中相邻 endpos 的最小差为 d,若最长串 len 满足 lend,则该等价类能够做出的最大贡献为 len+dd

使用线段树合并求出 endpos 最小差即可。

Luogu P4482 [BJWC2018] Border 的四种求法#

所求即最大的 i[l,r) 使得 lcp(s[1,i],s[1,r])il+1

在 SAM 的 link 树上标记出所有前缀节点。对于询问 [l,r],记 s[1,x] 对应节点为 edx,则 i 需要满足的第二条限制可以写作 dep(lca(edi,edr))il+1。枚举 u=lca(edi,edr),则对于 i 的限制形如 i<depu+l,i[l,r)。link 树树高为线性,直接遍历 edr 的所有祖先,复杂度 O(n),难以通过。

考虑树链剖分。对于每条重链考虑,此时合法的 u 是这条重链的一个前缀。设该前缀为链顶 top 到某个节点 x,则 i 的来源有三种情况:

  1. i 对应的节点位于 x 子树内。

  2. i 对应的节点位于重链链顶到 x 这一段。

  3. i 对应的节点位于重链链顶到 x 这一段的某个节点的轻子树中。

对于情况 1,此时的 depu 就是 depx。(如果实际的 LCA 深度更深,则会在更靠下的重链中被正确计算贡献。实际贡献是更大的,因此不需要担心这里计算错误)对 SAM 上节点做可持久化线段树合并,则容易在 x 的线段树中查询出最大的 i 满足 i<depx+l,i[l,r)

对于情况 2 和情况 3,从链头开始,依次加入重链上的节点 t 以及 t 的轻子树到一棵线段树中,直到 x 为止(包括 x)。具体地,加入某个节点时,若它是代表前缀 [1,d] 的节点,则在线段树上的位置 d 插入值 ddept。最后,线段树上查询所有小于 r 的位置中,值 ddept 小于 l 的最大位置是多少。若该位置不小于 l,则它可以作为一个合法的 i

该做法的优势在于,如果有多组询问,我们可以离线下来,将对于某段重链的查询挂在节点 x 上,然后整体利用同一棵线段树计算答案。假设 n,q 同阶,则处理询问的复杂度为 O(nlog2n) 。现在考虑对于情况 2,3,遍历重链和轻子树的复杂度。根据重链剖分的性质,全体重链的长度和为 n,全体重链轻子树的大小为 O(nlogn)。那么,一共只会有 O(nlogn) 次加点,从而这部分的复杂度也是 O(nlog2n)。这样,我们便在 O(nlog2n) 的时间复杂度,O(nlog2n)O(nlogn)(采用更高明的线段树合并)的空间复杂度内解决了本题。

P6816 [PA2009] Quasi-template#

题解 Luogu P6816 [PA2009] Quasi-template - Meatherm

P6292 区间本质不同子串个数#

考虑类似 HH 的项链的套路,区间数颜色问题我们钦定每个点都在最后出现的位置贡献。离线 + 扫描线,考虑右端点 r 往右移一个位置到 r+1,所对应到的前缀节点为 u,那么 uu Parent 树上所有祖先的出现位置都会变成 r+1。如果一个串 T 最后一次出现的位置为 p,那么 l[1,|T|p+1]l 的答案就会加上 1

这非常像 LCT 的 access 操作,于是我们就用 LCT 来维护这个东西。在 access 时,首先需要删掉这条链之前的贡献,再加入新的贡献。考虑 Parent 树上任意一条垂直链所代表的字符串长度连续,即:|T|[L,R],因此对 l 的答案的贡献形式是区间加等差数列。因为只需要单点查询,考虑差分之后变成区间加法,线段树维护即可。


广义 SAM 部分#

https://www.cnblogs.com/Xing-Ling/p/12038349.html

https://www.luogu.com/discuss/322224

注意品鉴其中错误的 SAM。这里面讲了为什么只把 last 指回 1 是错的。其原因是,插入多串时,新加的那个前缀不一定不存在。如果存在,就不应该再加一个节点进去了,直接判断是否要分裂即可。

当然有多个串的时候复杂度证明就变得微妙了起来,不过我们可以感性理解一下。如果需要严格证明可以阅读论文。

Luogu P6139 【模板】广义后缀自动机(广义 SAM)#

广义 SAM 模板题。

Luogu P3346 [ZJOI2015] 诸神眷顾的幻想乡#

注意到每条路径必然存在一个叶子,使得以该叶子为根的时候这条路径的形态是自上而下。

叶子数量很少,因此直接暴力枚举叶子为根,然后将串插入到广义 SAM 中即可。

Codeforces 452E Three strings#

建出广义 SAM。分别计算出每个节点,来源于三个串的 endpos 数量 (a,b,c),则该节点会为该节点等价类中每一个长度 abc 即为该节点的贡献。

P4081 [USACO17DEC] Standing Out from the Herd P#

建出广义 SAM。给每个字符串的每个节点打上属于该字符串的标记,如果某一时刻某个节点有两种标记,则标记为不可用。接着向上传递标记,如果子树内有两种标记或子树内有不可用节点,则该节点也不可用。可以通过 DFS link 树在线性时间内完成。

SPOJ JZPGYZ - Sevenk Love Oimaster#

不难转化为广义 SAM 上子树数颜色问题,使用线段树合并 / 启发式合并等方式 polylog 解决。

接下来介绍广义 SAM 上的自然根号。

事实上,设 S 是模式串集合,模式串总长为 L,则 S 的 SAM 上的节点 x 满足

sSx[longest(x)s]O(L1.5)

即对于所有模式串,SAM 上被该串包含的状态总和是 O(LL) 的。

考虑证明:s 的本质不同子串个数最多有 |s|2 个,SAM 上最多有 L 个节点,因此对于一个串 s,它在 SAM 上最多包含 min{|s|2,L} 个节点。

考虑对于长度大于 L 的串,最多有 L 个,这一部分的贡献不超过 L×L=L1.5

对于长度小于 L 的串,分析可知恰有 L 个长 L 的串时贡献最大,同样不超过 L1.5

综上,上式是 O(L1.5) 的,事实上这个上界很紧,存在随机构造使得上式达到 0.5L1.5,详见 Itst 的博客。

那么这个东西有什么用呢?它表明,考虑所有模式串 s 的前缀节点,它们在 link 树上的到根路径并的大小之和是 O(LL) 的。因此,如果我们对于每个模式串 s 执行:找到 s 的所有前缀节点,对于每个节点,在 link 树上从它开始向上跳,给每个节点打上关于 s 的标记,如果有关于 s 的标记就停止,该结论表明其时间复杂度是 O(LL) 的。

P4022 [CTSC2012]熟悉的文章#

记字典为 D。对于一个询问串 S,考虑二分答案 L。设 f(i) 表示前 i 个字符最多能匹配上多少个,则当 f(|S|)0.9|S| 时,答案 L 合法,否则非法。

考虑如何求出 f(i)。设 li 表示 S[1,i] 的最长后缀长度使得 S[ili+1,i] 作为 D 中某个串的子串出现过。则 f(i)=max(f(i1),maxj=iliiLf(j)+ij)。根据 li 的定义,显然 ili 单调不降(若 ili<(i1)li1,则 li>li1+1,矛盾)同时 iL 单调不降,因此这是左右端点均不降的滑动窗口问题,使用单调队列优化 DP 解决。

只需要将 SD 的广义 SAM 上匹配即可求出 li。这个过程是线性的,因此总复杂度为线性对数。

Codeforces 666E Forensic Examination#

T 建出广义 SAM 后跑线段树合并,求出每个等价类的 endpos 集合中,属于每个字符串的 endpos 各有多少个。

考虑对于每个 r,维护出 S[1,r] 在 SAM 上匹配能够到达的状态 s(r)。查询 S[pl,pr] 时,在 link 树上从 s(pr) 开始倍增求出最靠上的状态使得其为 s(pr) 的祖先且该节点长度不小于 (prpl+1)。查询该节点线段树上 [l,r] 区间的最大值即可。

作者:Meatherm

出处:https://www.cnblogs.com/Meatherm/p/18221137

版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。

posted @   Meatherm  阅读(27)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
· 写一个简单的SQL生成工具
· Manus的开源复刻OpenManus初探
more_horiz
keyboard_arrow_up light_mode palette
选择主题
menu
点击右上角即可分享
微信分享提示