后缀自动机学习笔记

作用

后缀自动机\((SAM)\)是一个能解决许多字符串相关问题的有力的数据结构

它可以把一个字符串的所有子串都表示出来

而且从根出发的任意一条合法路径都是该串中的一个子串

构造

要表示一个字符串所有的子串,最简单的方法是对于所有的子串建一棵字典树

但是这样做时间复杂度和空间复杂度都是 \(O(n^2)\) 的

因此考虑把一些没有用的节点和边去掉

定义 \(endpos(p)\) 为一个子串 \(p\) 出现的所有位置的右端点标号组成的集合

关于 \(endpos\) 有如下的性质

\(1\)、如果两个子串的 \(endpos\) 相同,则其中子串一个必然为另一个的后缀

\(2\)、对于任意两个子串 \(t\) 和 \(p\)\(( len_t\le len_p)\),要么\(endpos(t)\in endpos(p)\),要么 \(endpos(t) \bigcap endpos(p)=\emptyset\)

\(3\)、对于 \(endpos\) 相同的子串,我们将它们归为一个 \(endpos\) 等价类

对于任意一个 \(endpos\) 等价类,将包含在其中的所有子串依长度从大到小排序,则每一个子串的长度均为上一个子串的长度减 \(1\),且为上一个子串的后缀

简单来说,一个 \(endpos\) 等价类内的串的长度连续

\(4\)、\(endpos\) 等价类个数的级别为 \(O(n)\)

如果我们在一个字符串的前面添加两个不同的字符

可以把当前的集合分成两个没有交集的集合

类似于线段树的分割方法可以达到最大的划分数 \(2n\)

所以后缀自动机的空间要开 \(2\) 倍

如果把母集作为分割出来的小集合的父亲,就会形成一棵树,这就是 \(parent\ tree\)

\(parent\ tree\) 上的节点还有一个性质 \(minlen[now]=maxlen[fa]+1\)

这样对于每一个 \(endpos\) 等价类,只需要记录属于它的字符串的最大长度即可,最小长度可以由父亲节点推出来

后缀自动机是在 \(parent\) 树的基础上构建而来的,\(parent\) 树的节点就是后缀自动机的节点

当然,后缀自动机不仅有 \(parent\ tree\) 的父子关系,也有 \(trie\) 树那样的转移边

沿着一个节点通过一条转移边到达另一个节点代表着在当前字符串后面添加字符

从 \(parent\ tree\) 上的父亲节点达到儿子节点则代表着在当前字符串前面添加字符

具体构造的时候采用增量法构造

rg int p=lst;
rg int np=lst=++cnt;
len[np]=len[p]+1;

设当前已经构造到了字符串的第 \(n\) 个字符

首先记录一下添加第 \(n-1\) 个字符时新建的节点 \(p\)

对于当前的位置新开一个节点 \(np\),代表着只含有位置 \(n\) 的 \(endpos\) 集合

显然字符串 \(s[1 \cdots n]\) 在之前一定没有出现过,所以它一定属于 \(np\) 这个点

所以这个集合中最长的字符串是 \(s[1 \cdots n]\),也就是 \(s[1 \cdots n-1]+1\)

for(;p && !ch[p][c];p=fa[p]) ch[p][c]=np;
if(!p) fa[np]=1;

现在我们要考虑的就是 \(s[2 \cdots n],s[3 \cdots n] \cdots s[n \cdots n]\) 属于哪一个 \(endpos\) 集合

因为我们不知道它们在之前有没有出现过

所以去跳 \(np\) 在 \(parent\ tree\) 上的父亲

含义就是字符串 \(s[1 \cdots n-1]\) 在前面不断去掉字符

看 \(s[1 \cdots n-1],s[2 \cdots n-1] \cdots s[n-1 \cdots n-1]\) 能不能在后面添加一个字符 \(s[n]\)到达一个已有的状态

如果不能,就代表着当前长度以 \(n\) 结尾的字符串还没有出现过,那么它显然也属于 \(np\) 这个集合

同时由 \(p\) 向 \(np\) 建一条边,代表着可以在当前字符串的后面添加字符

如果都到根节点了还没有找到一个之前已经出现过的字符串

只能说明 \(s[n]\) 这个种类的字符之前没有出现

直接把它连向根节点就行了

rg int q=ch[p][c];
if(len[q]==len[p]+1) fa[np]=q;

否则说明以 \(n\) 为结尾的子串之前出现过一部分

这些已经出现过的子串的 \(endpos\) 肯定不是单独的一个 \(n\)

所以就不能把它归到 \(np\) 这个集合中了

而且我们并不知道这些以 \(s[n]\) 结尾的子符串是不是 \(s[1 \cdots n]\) 的子串

有可能全部是,也有可能只有一部分是

如果全部是的话,我们就不需要新开一个 \(endpos\) 集合了

只要在之前的 \(endpos\) 集合的基础上添一个 \(n\) 就行了

否则我们就要把这两部分分开,一部分含有 \(n\),一部分不含有 \(n\)

判断的标准就是 \(len[q]=len[p]+1\)

因为我们由 \(p\) 到 \(q\) 的含义就是在一个以 \(s[n-1]\) 结尾的字符串的后面加一个字符变成以 \(s[n]\) 结尾的字符串

如果出现了 \(q\) 中最长字符串不是仅仅添加了一个字符得到的情况

说明这个最长字符串一定不是以 \(s[n]\) 结尾的

反之亦然

如果满足条件就很好办了,直接把 \(np\) 的父亲设为 \(q\)

而且此时我们不用去跳 \(p\) 的父亲了

因为 \(p\) 的父亲节点的出边指向的节点一定全是以 \(s[n]\) 结尾的

而且这些字符串一定是 \(q\) 中字符串的后缀

它们的 \(endpos\) 集合都整体加了 \(n\)

rg int nq=++cnt;
len[nq]=len[p]+1;
memcpy(ch[nq],ch[q],sizeof(ch[q]));
fa[nq]=fa[q];
fa[q]=fa[np]=nq;
for(;p && ch[p][c]==q;p=fa[p]) ch[p][c]=nq;

如果不满足呢

肯定要把两个集合分开

新建一个点 \(nq\) 存储 \(endpos\) 多了 \(n\) 的字符串

显然这些字符串中长度最长的就是 \(len[p]+1\)

所以 \(len[nq]=len[p]+1\)

因为我们只是把原来的集合一分为二,所以原来集合的出边直接让两个儿子继承就行了

但是父子关系要变一下

因为 \(nq\) 和 \(q\) 是由一个集合分出来的,它们肯定满足后缀关系

因为 \(nq\) 的长度更短并且 \(endpos\) 集合中的元素更多

所以要把 \(q\) 的父亲设为 \(nq\)

同理 \(np\) 的父亲也是 \(nq\)

最后再把本应该指向 \(nq\) 的边改过来就行了

void insert(rg int c){
	rg int p=lst;
	rg int np=lst=++cnt;
	len[np]=len[p]+1;
	for(;p && !ch[p][c];p=fa[p]) ch[p][c]=np;
	if(!p) fa[np]=1;
	else {
		rg int q=ch[p][c];
		if(len[q]==len[p]+1) fa[np]=q;
		else {
			rg int nq=++cnt;
			len[nq]=len[p]+1;
			memcpy(ch[nq],ch[q],sizeof(ch[q]));
			fa[nq]=fa[q];
			fa[q]=fa[np]=nq;
			for(;p && ch[p][c]==q;p=fa[p]) ch[p][c]=nq;
		}
	}
	siz[np]=1;
}

这样建造出来的后缀自动机是一个\(DAG\)(\(AC\)自动机则是一个 \(Trie\) 图)

和回文自动机不同,长度长的不一定编号大,也就是说 \(1 \sim n\) 不一定是一个拓扑序

所以还要按照长度进行桶排,得到的才是最终的拓扑序

广义后缀自动机

和普通的后缀自动机同理

加入一个新的字符串之前,要把 \(lst\) 重置成 \(1\)

对于之前已经出现过的节点不要重复去建就行了

要注意的是广义后缀自动机不能按照桶排来确定拓扑序

要用正常的队列的写法

struct SAM{
	int ch[maxn][28],fa[maxn],len[maxn],cnt,siz[maxn];
	int insert(rg int lst,rg int c){
		rg int p=lst;
		if(ch[p][c]){
			rg int q=ch[p][c];
			if(len[q]==len[p]+1) return q;
			else {
				rg int nq=++cnt;
				len[nq]=len[p]+1;
				memcpy(ch[nq],ch[q],sizeof(ch[q]));
				fa[nq]=fa[q];
				fa[q]=nq;
				for(;p && ch[p][c]==q;p=fa[p]) ch[p][c]=nq; 
				return nq;
			}
		}
		rg int np=++cnt;
		len[np]=len[p]+1;
		for(;p && !ch[p][c];p=fa[p]) ch[p][c]=np;
		if(!p) fa[np]=1;
		else {
			rg int q=ch[p][c];
			if(len[q]==len[p]+1) fa[np]=q;
			else {
				rg int nq=++cnt;
				len[nq]=len[p]+1;
				memcpy(ch[nq],ch[q],sizeof(ch[q]));
				fa[nq]=fa[q];
				fa[q]=fa[np]=nq;
				for(;p && ch[p][c]==q;p=fa[p]) ch[p][c]=nq; 
			}
		}
		return np;
	}
	void build(){
		cnt=1;
		n=read();
		for(rg int i=1;i<=n;i++){
			scanf("%s",s+1);
			rg int nlen=strlen(s+1);
			for(rg int lst=1,j=1;j<=nlen;j++){
				lst=insert(lst,s[j]-'a'+1);
				siz[lst]++;
			}
		}
	}
	void calc(){
		rg long long ans=0;
		for(rg int i=1;i<=cnt;i++){
			ans+=len[i]-len[fa[i]];
		}
		printf("%lld\n",ans);
	}
}sam;

例题

P3181 [HAOI2016]找相同字符

题目传送门

分析

广义后缀自动机的模板题

对于节点 \(i\),它代表的 \(endpos\) 集合中本质不同的字符串一共有 \(len[i]-len[fa[i]]\) 个

对于两个字符串,分别记录一下它们在某个 \(endpos\) 集合中出现的次数

最终的答案就是 \((len[i]-len[fa[i]]) \times siz[i][0] \times siz[i][1]\)

代码

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
#define rg register
inline int read(){
	rg int x=0,fh=1;
	rg char ch=getchar();
	while(ch<'0' || ch>'9'){
		if(ch=='-') fh=-1;
		ch=getchar();
	}
	while(ch>='0' && ch<='9'){
		x=(x<<1)+(x<<3)+(ch^48);
		ch=getchar();
	}
	return x*fh;
}
const int maxn=2e6+5;
char s[maxn];
struct SAM{
	int ch[maxn][28],fa[maxn],len[maxn],cnt,siz[maxn][2],rd[maxn];
	std::queue<int> q;
	int insert(rg int lst,rg int c){
		rg int p=lst;
		if(ch[p][c]){
			rg int q=ch[p][c];
			if(len[q]==len[p]+1) return q;
			else {
				rg int nq=++cnt;
				len[nq]=len[p]+1;
				memcpy(ch[nq],ch[q],sizeof(ch[q]));
				fa[nq]=fa[q];
				fa[q]=nq;
				for(;p && ch[p][c]==q;p=fa[p]) ch[p][c]=nq; 
				return nq;
			}
		}
		rg int np=++cnt;
		len[np]=len[p]+1;
		for(;p && !ch[p][c];p=fa[p]) ch[p][c]=np;
		if(!p) fa[np]=1;
		else {
			rg int q=ch[p][c];
			if(len[q]==len[p]+1) fa[np]=q;
			else {
				rg int nq=++cnt;
				len[nq]=len[p]+1;
				memcpy(ch[nq],ch[q],sizeof(ch[q]));
				fa[nq]=fa[q];
				fa[q]=fa[np]=nq;
				for(;p && ch[p][c]==q;p=fa[p]) ch[p][c]=nq; 
			}
		}
		return np;
	}
	void build(){
		cnt=1;
		for(rg int i=0;i<=1;i++){
			scanf("%s",s+1);
			rg int nlen=strlen(s+1);
			for(rg int lst=1,j=1;j<=nlen;j++){
				lst=insert(lst,s[j]-'a'+1);
				siz[lst][i]++;
			}
		}
		for(rg int i=1;i<=cnt;i++) rd[fa[i]]++;
		for(rg int i=1;i<=cnt;i++) if(rd[i]==0) q.push(i);
		while(!q.empty()){
			rg int now=q.front();
			q.pop();
			siz[fa[now]][0]+=siz[now][0],siz[fa[now]][1]+=siz[now][1];
			--rd[fa[now]];
			if(rd[fa[now]]==0) q.push(fa[now]);
		}
	}
	void calc(){
		rg long long ans=0;
		for(rg int i=1;i<=cnt;i++){
			ans+=1LL*(len[i]-len[fa[i]])*siz[i][0]*siz[i][1];
		}
		printf("%lld\n",ans);
	}
}sam;
int main(){
	sam.build();
	sam.calc();
	return 0;
}

P4081 [USACO17DEC]Standing Out from the Herd P

题目传送门

分析

用线段树合并维护每一个 \(emdpos\) 集合中有多少个字符串出现过

如果只有一种字符串出现过就累加答案

注意有些情况下后缀自动机上的线段树合并和普通的线段树合并有所不同

普通的线段树合并会破坏原来两颗线段树的形态,想要查询之前的信息就不准确了

所以合并的时候要新开一个节点

这道题因为是边查询边统计答案,所以用正常的写法就行

代码

#include<cstdio>
#include<cstring>
#include<iostream>
#define rg register
inline int read(){
	rg int x=0,fh=1;
	rg char ch=getchar();
	while(ch<'0' || ch>'9'){
		if(ch=='-') fh=-1;
		ch=getchar();
	}
	while(ch>='0' && ch<='9'){
		x=(x<<1)+(x<<3)+(ch^48);
		ch=getchar();
	}
	return x*fh;
}
const int maxn=1e6+5;
struct trr{
	int lch,rch,siz;
}tr[maxn*20];
int rt[maxn],cnt,n;
void push_up(rg int da){
	tr[da].siz=tr[tr[da].lch].siz+tr[tr[da].rch].siz;
}
int ad(rg int da,rg int l,rg int r,rg int wz){
	if(!da) da=++cnt;
	if(l==r){
		tr[da].siz=1;
		return da;
	}
	rg int mids=(l+r)>>1;
	if(wz<=mids) tr[da].lch=ad(tr[da].lch,l,mids,wz);
	else tr[da].rch=ad(tr[da].rch,mids+1,r,wz);
	push_up(da);
	return da;
}
int bing(rg int aa,rg int bb,rg int l,rg int r){
	if(!aa || !bb) return aa+bb;
	if(l==r){
		tr[aa].siz+=tr[bb].siz;
		tr[aa].siz=1;
		return aa;
	}
	rg int mids=(l+r)>>1;
	tr[aa].lch=bing(tr[aa].lch,tr[bb].lch,l,mids);
	tr[aa].rch=bing(tr[aa].rch,tr[bb].rch,mids+1,r);
	push_up(aa);
	return aa;
}
int cx(rg int da,rg int l,rg int r){
	if(l==r) return l;
	rg int mids=(l+r)>>1;
	if(tr[tr[da].lch].siz) return cx(tr[da].lch,l,mids);
	else return cx(tr[da].rch,mids+1,r);
}
char s[maxn];
struct SAM{
	int ch[maxn][28],fa[maxn],cnt,len[maxn],tax[maxn],a[maxn],ans[maxn];
	int insert(rg int lst,rg int c){
		rg int p=lst;
		if(ch[p][c]){
			rg int q=ch[p][c];
			if(len[q]==len[p]+1) return q;
			else {
				rg int nq=++cnt;
				len[nq]=len[p]+1;
				memcpy(ch[nq],ch[q],sizeof(ch[q]));
				fa[nq]=fa[q];
				fa[q]=nq;
				for(;p && ch[p][c]==q;p=fa[p]) ch[p][c]=nq;
				return nq;
			}
		}
		rg int np=++cnt;
		len[np]=len[p]+1;
		for(;p && !ch[p][c];p=fa[p]) ch[p][c]=np;
		if(!p){
			fa[np]=1;
		} else {
			rg int q=ch[p][c];
			if(len[q]==len[p]+1) fa[np]=q;
			else {
				rg int nq=++cnt;
				len[nq]=len[p]+1;
				memcpy(ch[nq],ch[q],sizeof(ch[q]));
				fa[nq]=fa[q];
				fa[q]=fa[np]=nq;
				for(;p && ch[p][c]==q;p=fa[p]) ch[p][c]=nq;
			}
		}
		return np;
	}
	void build(){
		cnt=1;
		n=read();
		for(rg int i=1;i<=n;i++){
			scanf("%s",s+1);
			rg int nlen=strlen(s+1);
			for(rg int j=1,lst=1;j<=nlen;j++){
				lst=insert(lst,s[j]-'a'+1);
				rt[lst]=ad(rt[lst],1,n,i);
			}
		}
	}
	void calc(){
		for(rg int i=1;i<=cnt;i++) tax[len[i]]++;
		for(rg int i=1;i<=cnt;i++) tax[i]+=tax[i-1];
		for(rg int i=1;i<=cnt;i++) a[tax[len[i]]--]=i;
		for(rg int i=cnt;i>=1;i--){
			rg int p=a[i];
			if(tr[rt[p]].siz==1){
				ans[cx(rt[p],1,n)]+=len[p]-len[fa[p]];
			} 
			rt[fa[p]]=bing(rt[fa[p]],rt[p],1,n);
		}
		for(rg int i=1;i<=n;i++){
			printf("%d\n",ans[i]);
		}
	}
}sam;
int main(){
	sam.build();
	sam.calc();
	return 0;
}

P4770 [NOI2018] 你的名字

题目传送门

分析

给你一个字符串 \(S\), 有很多组询问, 每次给定一个 \(T\), 求 \(T\) 中不在 \(S[l:r]\) 中出现的本质不同的子串个数

用 \(T\) 的本质不同的子串减去 \(T\) 在 \(S[l:r]\) 中出现的本质不同的子串个数

前者很好求,对于 \(T\) 建出后缀自动机,答案就是 \(\sum len[i]-len[fa[i]]\)

关键在于如何求出后面的部分

先考虑最简单的匹配问题

即对于一个字符串 \(T\),求出它与另一个字符串 \(S\) 的最长公共子串

对于 \(S\) 建立后缀自动机,把初始的位置置为根节点

对于 \(T\) 从第一个字符开始枚举

如果后缀自动机的节点上有当前字符的出边就一直走下去,同时把匹配长度加一

否则就一直跳 \(parent\ tree\) 直到匹配上为止,把匹配长度置为当前节点的长度

如果到了根节点还匹配不上,就把匹配长度置为 \(0\)

now=1,cs=0;
for(rg int i=1;i<=n;i++){
	rg int p=s[i]-'a'+1;
	while(now && !ch[now][p]) now=fa[now],cs=len[now];
	if(!now){
		now=1;
		cs=0;
	} else {
		cs++;
		now=ch[now][p];
	}
	ans=std::max(ans,cs);
}

现在无非是在普通匹配的基础上加上了 \([l,r]\) 的限制

只要用线段树合并维护一下 \(endpos\) 集合即可,这里要写新开节点的那一种

在线段树上查询当前的节点在 \([l,r]\) 中能匹配的最靠右的端点

如果一直不存在就一直向上跳

跳到第一个合法的位置之后还要继续向上跳

因为越往上 \(endpos\) 集合中含有的元素越多

查询右端点时得到的答案也就越靠右

一直跳到当前节点的长度限制答案为止

还有一个问题就是如何去重

因为不同位置本质相同的字符串只算一次

这时候我们就需要对于 \(T\) 建一个后缀自动机

这样就可以求出以每一个节点为结尾的第一次出现的字符串的个数

每一位匹配的长度和这个值取一个 \(min\) 即可

代码

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
#define rg register
const int maxn=1e6+5;
char s[maxn];
int n,t,rt[maxn],trcnt,l,r;
struct trr{
	int lch,rch,mmax;
}tr[maxn*20];
void push_up(rg int da){
	tr[da].mmax=std::max(tr[tr[da].lch].mmax,tr[tr[da].rch].mmax);
}
int ad(rg int da,rg int l,rg int r,rg int wz){
	da=++trcnt;
	if(l==r){
		tr[da].mmax=wz;
		return da;
	}
	rg int mids=(l+r)>>1;
	if(wz<=mids) tr[da].lch=ad(tr[da].lch,l,mids,wz);
	else tr[da].rch=ad(tr[da].rch,mids+1,r,wz);
	push_up(da);
	return da;
}
int bing(rg int aa,rg int bb,rg int l,rg int r){
	if(!aa || !bb) return aa+bb;
	rg int cc=++trcnt,mids=(l+r)>>1;
	if(l==r){
		tr[cc].mmax=std::max(tr[aa].mmax,tr[bb].mmax);
		return cc;
	}
	tr[cc].lch=bing(tr[aa].lch,tr[bb].lch,l,mids);
	tr[cc].rch=bing(tr[aa].rch,tr[bb].rch,mids+1,r);
	push_up(cc);
	return cc;
}
int cx(rg int da,rg int l,rg int r,rg int L,rg int R){
	if(!da) return -1;
	if(l>=L && r<=R) return tr[da].mmax;
	rg int nans=-1,mids=(l+r)>>1;
	if(L<=mids) nans=std::max(nans,cx(tr[da].lch,l,mids,L,R));
	if(R>mids) nans=std::max(nans,cx(tr[da].rch,mids+1,r,L,R));
	return nans;
}
struct SAM{
	int len[maxn],ch[maxn][28],fa[maxn],lst,cnt,id[maxn],tax[maxn],nlen,jl[maxn];
	void insert(rg int c){
		rg int p=lst;
		rg int np=lst=++cnt;
		len[np]=len[p]+1;
		for(;p && !ch[p][c];p=fa[p]) ch[p][c]=np;
		if(!p) fa[np]=1;
		else {
			rg int q=ch[p][c];
			if(len[q]==len[p]+1) fa[np]=q;
			else {
				rg int nq=++cnt;
				len[nq]=len[p]+1;
				memcpy(ch[nq],ch[q],sizeof(ch[q]));
				fa[nq]=fa[q];
				fa[q]=fa[np]=nq;
				for(;p && ch[p][c]==q;p=fa[p]) ch[p][c]=nq;
			}
		}
	}
	void build(rg int op){
		for(rg int i=1;i<=cnt;i++) fa[i]=0,memset(ch[i],0,sizeof(ch[i]));
		lst=cnt=1;
		nlen=strlen(s+1);
		for(rg int i=1;i<=nlen;i++){
			insert(s[i]-'a'+1);
			jl[i]=len[fa[lst]];
			if(!op) rt[lst]=ad(rt[lst],1,n,i);
		}
		if(!op){
			for(rg int i=1;i<=cnt;i++) tax[len[i]]++;
			for(rg int i=1;i<=nlen;i++) tax[i]+=tax[i-1];
			for(rg int i=1;i<=cnt;i++) id[tax[len[i]]--]=i;
			for(rg int i=cnt;i>=1;i--){
				rg int tmp=id[i];
				if(fa[tmp]) rt[fa[tmp]]=bing(rt[fa[tmp]],rt[tmp],1,n);
			}
		}
	}
}sam1,sam2;
long long solve(rg int l,rg int r){
	rg long long ans=0;
	for(rg int i=1;i<=sam2.cnt;i++) ans+=sam2.len[i]-sam2.len[sam2.fa[i]];
	rg int now=1,cs=0,tmp,nrt,nans=0;
	for(rg int i=1;i<=sam2.nlen;i++){
		rg int p=s[i]-'a'+1;
		tmp=cx(rt[sam1.ch[now][p]],1,n,l,r);
		while(now && tmp==-1){
			now=sam1.fa[now],cs=sam1.len[now];
			tmp=cx(rt[sam1.ch[now][p]],1,n,l,r);
		}
		if(!now) now=1,cs=0;
		else {
			now=sam1.ch[now][p],cs++,nrt=sam1.fa[now],nans=std::min(sam1.len[now],tmp-l+1);
			while(nrt){
				tmp=cx(rt[nrt],1,n,l,r);
				nans=std::max(nans,std::min(tmp-l+1,sam1.len[nrt]));
				if(tmp-l+1>=sam1.len[nrt]) break;
				nrt=sam1.fa[nrt];
			}
			nans=std::min(nans,cs);
			if(nans>sam2.jl[i]) ans-=(nans-sam2.jl[i]);
		}
	}
	return ans;
}
int main(){
	scanf("%s",s+1);
	n=strlen(s+1);
	sam1.build(0);
	scanf("%d",&t);
	rg int l,r;
	for(rg int i=1;i<=t;i++){
		scanf("%s%d%d",s+1,&l,&r);
		sam2.build(1);
		printf("%lld\n",solve(l,r));
	}
	return 0;
}

CF666E Forensic Examination

题目传送门

分析

对于字符串数组建立广义后缀自动机

查询之前先对于串 \(S\) 在后缀自动上跑一遍匹配

对于每一个位置记录以该位置结尾的后缀在后缀自动机上能够匹配的最长的位置以及对应的节点

对于每一个 \(endpos\) 集合用线段树记录一下它在哪些子串中出现过以及出现的次数

如果要查询 \(s[l,r]\) 在字符串组中的那一个字符串中出现的次数最多

从预处理出来的以 \(r\) 结尾的后缀能够匹配的最长的位置对应的节点进行树上倍增

找到第一个长度大于等于 \(r-l+1\) 的位置

此时这个位置所含有的字符串的种类一定是最多的

直接在对应的线段树上查询即可

代码

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<iostream>
#include<queue>
#define rg register
inline int read(){
	rg int x=0,fh=1;
	rg char ch=getchar();
	while(ch<'0' || ch>'9'){
		if(ch=='-') fh=-1;
		ch=getchar();
	}
	while(ch>='0' && ch<='9'){
		x=(x<<1)+(x<<3)+(ch^48);
		ch=getchar();
	}
	return x*fh;
}
const int maxn=1e6+5;
struct trr{
	int lch,rch,val,jl;
}tr[maxn*20];
int trcnt,rt[maxn];
void push_up(rg int da){
	if(tr[tr[da].lch].val<tr[tr[da].rch].val) tr[da].jl=tr[tr[da].rch].jl;
	else if(tr[tr[da].rch].val<tr[tr[da].lch].val) tr[da].jl=tr[tr[da].lch].jl;
	else tr[da].jl=std::min(tr[tr[da].lch].jl,tr[tr[da].rch].jl);
	tr[da].val=std::max(tr[tr[da].lch].val,tr[tr[da].rch].val);
}
int ad(rg int da,rg int l,rg int r,rg int wz){
	if(!da) da=++trcnt;
	if(l==r){
		tr[da].val++;
		tr[da].jl=wz;
		return da;
	}
	rg int mids=(l+r)>>1;
	if(wz<=mids) tr[da].lch=ad(tr[da].lch,l,mids,wz);
	else tr[da].rch=ad(tr[da].rch,mids+1,r,wz);
	push_up(da);
	return da;
}
int bing(rg int aa,rg int bb,rg int l,rg int r){
	if(!aa || !bb) return aa+bb;
	rg int cc=++trcnt,mids=(l+r)>>1;
	if(l==r){
		tr[cc].val=tr[aa].val+tr[bb].val;
		tr[cc].jl=l;
		return cc;
	}
	tr[cc].lch=bing(tr[aa].lch,tr[bb].lch,l,mids);
	tr[cc].rch=bing(tr[aa].rch,tr[bb].rch,mids+1,r);
	push_up(cc);
	return cc;
}
int cx(rg int da,rg int l,rg int r,rg int L,rg int R){
	if(!da) return 0;
	if(l>=L && r<=R) return da;
	rg int mids=(l+r)>>1,nans=0,tmp;
	if(L<=mids){
		tmp=cx(tr[da].lch,l,mids,L,R);
		if(tr[tmp].val>tr[nans].val) nans=tmp;
		else if(tr[tmp].val==tr[nans].val){
			if(tr[tmp].jl<tr[nans].jl) nans=tmp;
		}
	}
	if(R>mids){
		tmp=cx(tr[da].rch,mids+1,r,L,R);
		if(tr[tmp].val>tr[nans].val) nans=tmp;
		else if(tr[tmp].val==tr[nans].val){
			if(tr[tmp].jl<tr[nans].jl) nans=tmp;
		}
	}
	return nans;
}
char s1[maxn],s2[maxn];
int n,m,q,mat[maxn];
struct SAM{
	int fa[maxn],ch[maxn][28],len[maxn],cnt,nlen,rd[maxn],zx[maxn][22],sta[maxn],tp,mmax[maxn];
	std::queue<int> q;
	int insert(rg int lst,rg int c){
		rg int p=lst;
		if(ch[p][c]){
			rg int q=ch[p][c];
			if(len[q]==len[p]+1) return q;
			else {
				rg int nq=++cnt;
				len[nq]=len[p]+1;
				memcpy(ch[nq],ch[q],sizeof(ch[q]));
				fa[nq]=fa[q];
				fa[q]=nq;
				for(;p && ch[p][c]==q;p=fa[p]) ch[p][c]=nq;
				return nq;
			}
		}
		rg int np=++cnt;
		len[np]=len[p]+1;
		for(;p && !ch[p][c];p=fa[p]) ch[p][c]=np;
		if(!p) fa[np]=1;
		else {
			rg int q=ch[p][c];
			if(len[q]==len[p]+1) fa[np]=q;
			else {
				rg int nq=++cnt;
				len[nq]=len[p]+1;
				memcpy(ch[nq],ch[q],sizeof(ch[q]));
				fa[nq]=fa[q];
				fa[q]=fa[np]=nq;
				for(;p && ch[p][c]==q;p=fa[p]) ch[p][c]=nq;
			}
		}
		return np;
	}
	void build(){
		cnt=1;
		for(rg int i=1;i<=m;i++){
			scanf("%s",s2+1);
			nlen=strlen(s2+1);
			for(rg int j=1,lst=1;j<=nlen;j++){
				lst=insert(lst,s2[j]-'a'+1);
				rt[lst]=ad(rt[lst],1,m,i);
			}
		}
		for(rg int i=1;i<=cnt;i++) rd[fa[i]]++,zx[i][0]=fa[i];
		for(rg int i=1;i<=cnt;i++) if(rd[i]==0) q.push(i);
		while(!q.empty()){
			rg int now=q.front();
			q.pop();
			sta[++tp]=now;
			rt[fa[now]]=bing(rt[fa[now]],rt[now],1,m);
			rd[fa[now]]--;
			if(rd[fa[now]]==0) q.push(fa[now]);
		}
		for(rg int i=tp;i>=1;i--){
			rg int now=sta[i];
			for(rg int j=1;j<=20;j++){
				zx[now][j]=zx[zx[now][j-1]][j-1];
			}
		}
	}
	void pre(){
		rg int cs=0,now=1;
		for(rg int i=1;i<=n;i++){
			rg int p=s1[i]-'a'+1;
			while(now && !ch[now][p]) now=fa[now],cs=len[now];
			if(!now) now=1,cs=0;
			else now=ch[now][p],cs++;
			mat[i]=now,mmax[i]=cs;
		}
	}
	void solve(rg int l1,rg int r1,rg int l2,rg int r2){
		if(mmax[r2]<r2-l2+1){
			printf("%d 0\n",l1);
			return;
		}
		rg int now=mat[r2];
		for(rg int i=20;i>=0;i--){
			if(len[zx[now][i]]>=r2-l2+1) now=zx[now][i];
		}
		if(len[now]<r2-l2+1){
			printf("%d 0\n",l1);
			return;
		}
		rg int tmp=cx(rt[now],1,m,l1,r1);
		if(!tmp) printf("%d 0\n",l1);
		else printf("%d %d\n",tr[tmp].jl,tr[tmp].val);
	}
}sam;
int main(){
	scanf("%s",s1+1);
	n=strlen(s1+1);
	m=read();
	sam.build(),sam.pre();
	q=read();
	rg int l1,r1,l2,r2;
	for(rg int i=1;i<=q;i++){
		l1=read(),r1=read(),l2=read(),r2=read();
		sam.solve(l1,r1,l2,r2);
	}
	return 0;
}

P5212 SubString

题目传送门

分析

一个字符串出现的次数就是它子树内的权值和

强制在线要用 \(lct\) 维护

因为子树和不好维护

所以可以在每一次修改后把当前节点到根的路径都加上对应的权值

查询的时候只要单点查询就行了

代码

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#define rg register
inline int read(){
	rg int x=0,fh=1;
	rg char ch=getchar();
	while(ch<'0' || ch>'9'){
		if(ch=='-') fh=-1;
		ch=getchar();
	}
	while(ch>='0' && ch<='9'){
		x=(x<<1)+(x<<3)+(ch^48);
		ch=getchar();
	}
	return x*fh;
}
const int maxn=3e6+5;
char s[maxn];
std::string chars;
int mask;
void getit(int mask){
	scanf("%s",s);
	chars=s;
	for(int j=0;j<chars.length();j++){
		mask=(mask*131+j)%chars.length();
		char t=chars[j];
		chars[j]=chars[mask];
		chars[mask]=t;
	}
}
struct LCT{
	int ch[maxn][2],fa[maxn],sum[maxn],tag[maxn],rev[maxn],sta[maxn],tp;
	void push_down(rg int da){
		rg int lc=ch[da][0],rc=ch[da][1];
		if(rev[da]){
			rev[lc]^=1,rev[rc]^=1,rev[da]^=1;
			std::swap(ch[da][0],ch[da][1]);
		}
		if(tag[da]){
			if(lc){
				tag[lc]+=tag[da];
				sum[lc]+=tag[da];
			}
			if(rc){
				tag[rc]+=tag[da];
				sum[rc]+=tag[da];
			}
			tag[da]=0;
		}
	}
	bool isroot(rg int da){
		return (ch[fa[da]][0]!=da)&&(ch[fa[da]][1]!=da);
	}
	void xuanzh(rg int x){
		rg int y=fa[x];
		rg int z=fa[y];
		rg int k=(ch[y][1]==x);
		if(!isroot(y)){
			ch[z][ch[z][1]==y]=x;
		}
		fa[x]=z;
		ch[y][k]=ch[x][k^1];
		fa[ch[x][k^1]]=y;
		ch[x][k^1]=y;
		fa[y]=x;
	}
	void splay(rg int x){
		sta[tp=1]=x;
		for(rg int i=x;!isroot(i);i=fa[i]) sta[++tp]=fa[i];
		for(rg int i=tp;i>=1;i--) push_down(sta[i]);
		while(!isroot(x)){
			rg int y=fa[x];
			rg int z=fa[y];
			if(!isroot(y)){
				(ch[z][1]==y)^(ch[y][1]==x)?xuanzh(x):xuanzh(y);
			}
			xuanzh(x);
		}
	}
	void access(rg int x){
		for(rg int y=0;x;y=x,x=fa[x]){
			splay(x);
			ch[x][1]=y;
		}
	}
	void makeroot(rg int x){
		access(x);
		splay(x);
		rev[x]^=1;
		push_down(x);
	}
	int findroot(rg int x){
		access(x);
		splay(x);
		push_down(x);
		while(ch[x][0]){
			x=ch[x][0];
			push_down(x);
		}
		splay(x);
		return x;
	}
	void split(rg int x,rg int y){
		makeroot(x);
		access(y);
		splay(y);
	}
	void link(rg int x,rg int y){
		makeroot(x);
		if(findroot(y)!=x) fa[x]=y;
	}
	void cut(rg int x,rg int y){
		makeroot(x);
		if(findroot(y)==x && fa[y]==x && ch[y][0]==0){
			ch[x][1]=fa[y]=0;
		}
	}
}lct;
struct SAM{
	int ch[maxn][28],fa[maxn],lst,len[maxn],cnt;
	void insert(rg int c){
		rg int p=lst;
		rg int np=lst=++cnt;
		len[np]=len[p]+1;
		for(;p && !ch[p][c];p=fa[p]) ch[p][c]=np;
		if(!p){
			fa[np]=1;
			lct.link(np,fa[np]);
		} else {
			rg int q=ch[p][c];
			if(len[q]==len[p]+1){
				fa[np]=q;
				lct.link(np,fa[np]);
			} else {
				rg int nq=++cnt;
				len[nq]=len[p]+1;
				memcpy(ch[nq],ch[q],sizeof(ch[q]));
				lct.cut(q,fa[q]);
				lct.link(nq,fa[q]);
				lct.link(np,nq);
				lct.link(q,nq);
				fa[nq]=fa[q];
				fa[q]=fa[np]=nq;
				lct.splay(q);
				lct.sum[nq]=lct.sum[q];
				for(;p && ch[p][c]==q;p=fa[p]) ch[p][c]=nq;
			}
		}
		lct.split(np,1);
		lct.sum[1]++;
		lct.tag[1]++;
	}
	void build(){
		lst=cnt=1;
		scanf("%s",s+1);
		rg int nlen=strlen(s+1);
		for(int i=1;i<=nlen;i++){
			insert(s[i]-'A');
		}
	}
	void ad(){
		rg int nlen=chars.length();
		for(rg int i=0;i<nlen;i++) insert(chars[i]-'A');
	}
	int cx(){
		rg int now=1,nlen=chars.length();
		for(rg int i=0;i<nlen;i++){
			rg int p=chars[i]-'A';
			if(!ch[now][p]) return 0;
			now=ch[now][p];
		}
		lct.splay(now);
		return lct.sum[now];
	}
}sam;
int q;
int main(){
	q=read();
	sam.build();
	for(rg int i=1;i<=q;i++){
		scanf("%s",s+1);
		if(s[1]=='A'){
			getit(mask);
			sam.ad();
		} else {
			getit(mask);
			rg int nans=sam.cx();
			printf("%d\n",nans);
			mask^=nans;
		}
	}
	return 0;
}

P4248 [AHOI2013]差异

题目传送门

分析

在后缀自动机中两个字符串中的 \(lcp\) 就是它们在 \(fail\) 树上的最近公共祖先

题目给出的式子其实就是两两之间的路径长度

枚举每一条边的贡献加起来即可

代码

#include<cstdio>
#include<cstring>
#include<algorithm>
#define rg register
const int maxn=4e6+5;
char s[maxn];
long long ans;
struct SAM{
	int ch[maxn][28],fa[maxn],lst,cnt,len[maxn],a[maxn],tax[maxn],siz[maxn],n;
	void insert(rg int c){
		rg int p=lst;
		rg int np=lst=++cnt;
		len[np]=len[p]+1;
		for(;p && !ch[p][c];p=fa[p]) ch[p][c]=np;
		if(!p) fa[np]=1;
		else {
			rg int q=ch[p][c];
			if(len[q]==len[p]+1) fa[np]=q;
			else {
				rg int nq=++cnt;
				len[nq]=len[p]+1;
				memcpy(ch[nq],ch[q],sizeof(ch[q]));
				fa[nq]=fa[q];
				fa[q]=fa[np]=nq;
				for(;p && ch[p][c]==q;p=fa[p]) ch[p][c]=nq;
			}
		}
		siz[np]=1;
	}
	void build(){
		scanf("%s",s+1);
		n=strlen(s+1);
		std::reverse(s+1,s+1+n);
		lst=cnt=1;
		for(rg int i=1;i<=n;i++) insert(s[i]-'a'+1);
	}
	void calc(){
		for(rg int i=1;i<=cnt;i++) tax[len[i]]++;
		for(rg int i=1;i<=cnt;i++) tax[i]+=tax[i-1];
		for(rg int i=1;i<=cnt;i++) a[tax[len[i]]--]=i;
		for(rg int i=cnt;i>=1;i--){
			rg int p=a[i];
			siz[fa[p]]+=siz[p];
			ans+=1LL*(len[p]-len[fa[p]])*siz[p]*(n-siz[p]);
		}
		printf("%lld\n",ans);
	}
}sam;
int main(){
	sam.build();
	sam.calc();
	return 0;
}
上一篇:单词extractor


下一篇:LuoguP5576 [CmdOI2019]口头禅 后缀树+线段树+暴力