[补档题解]后缀树节点数

Posted bestwyj

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[补档题解]后缀树节点数相关的知识,希望对你有一定的参考价值。

题目描述

给定一个长度为 (n) 的字符串 (P),有 (m) 次询问,每次给定两个参数 (l) , (r),询问子串 (P[l,r]) 所构成的后缀树的结点数。

(nle 10^5,mle 3 imes 10^5)

题解

tag:分类计数;后缀树/后缀自动机;线段树/树状数组;哈希。

做法来自2018集训队论文集。

由于构建后缀树需要倒转原串之后建立 SAM 得到,为方便处理,故我们先倒转原串以及对应询问区间。

下文例子的字符串都是倒转之后的。

(node_{[l,r]}) 表示找区间 ([l,r]) 对应子串在后缀树上的节点。

考虑传统后缀树节点数的计数方法,对于一个区间 ([l,r]) 建后缀自动机的过程,产生出了 np 和 nq 两种类型的节点。

np 节点对应的是 (iin [l,r]) 的前缀 ([l,i]) 对应的串,暂时称作前缀节点

nq 节点对应的是后缀树上必要的分叉,暂时称为分裂节点

example:"pabcqabc"

对于 "abc" 这个串,子树分成 "pabc" 和 "qabc" 两个独立的节点,故 "abc" 本身新建了一个分裂型节点。

考虑容斥,计算前缀节点个数 + 分裂节点个数 - 前缀&分裂节点的个数。

前缀节点个数

可以直接得到,即 (r-l+1)

分裂节点个数

放到后缀树上考虑,依然以上串 "pabcqabc" 为例:

"abc" 这个串在树上产生分叉(你考虑后缀树中实际相当于插入了 "cbap" 跟 "cbaqcbap" 俩串)的必要条件是其在当前区间中出现 (ge 2) 次,且存在两个出现位置,其往前一个位置的字符不同。

预处理整个串的后缀自动机以及树,离线询问。

考虑按右端点扫描线,扫到 (r) 的时候,将 ([1,r]) 对应的字符串 (node_{[l,r]}) 标记上 (r) 这个位置。

现在等价于求有多少个节点 (u),当前存在两个不同子树中的 (pos_a)(pos_b) 使得 (pos_a -len_uge l)(pos_b-len_uge l),那么 (u) 对应的节点将在 ([l,r]) 中作为分裂节点出现。

这个是个传统树上数据结构题,一种好写的方法是 lct 维护后缀树,access 更新过程中从 (root)(node_{[1,r]}) 的路径上染上 (r) 这种颜色,再维护每个节点来自不同子树最后两次染上的颜色分别是 (lst_u)(now_u),然后再用树状数组维护树上所有节点的 (lst_u-len_u),这样查询树状数组上 (ge l) 的部分的和就是分裂节点的个数。

这一段时间复杂度是 (O(nlog^2 n+mlog n)),来自于 lct 和树状数组。

接下来还需要计算前缀&分裂节点的个数。

考虑一种可行的暴力,枚举所有 (iin [l,r]) 找到 (node_{[l,i]}) 的位置 (p)(※),然后再判断该位置是否有两个或以上子树,然后再判断是否 (lst_pgt mid),仅当全部为“是”就对答案带来 (-1) 的贡献。

(※)笔者注:这个位置不一定恰好是树上的点,可能在边上,在边上显然不满足有两个或以上子树。这个位置就是对应的前缀节点将产生的位置,也就意味着,对 ([l,r]) 这个区间建后缀树,所有的分裂节点一定是整个长串的后缀树上节点的子集,而 (r-l+1) 个前缀节点不一定全是。

接下来将该暴力优化至 (O(mlog n)) 的复杂度:

引理:所有可行的 (iin [l,r]) 是一段前缀。

证明:显然一个串 ([l,i]) 能找到两个出现位置,其往前一个位置的字符不同,则 ([l,i-1]) 也可以。

example:"abcdabceabc"

其中 "abc" 作为分裂&前缀节点出现,可以直接说明前三个前缀节点均算重了。

有了引理,就可以上一个二分,每次 check ([l,mid]) 这个串是否形成分裂节点即可。

在后缀自动机上找 (node_{[l,mid]}) ,传统方法是利用倍增预处理,单次以 (O(log n)) 的复杂度查询;

判定分裂节点可以直接用上一部分算出的 (lst_u) 来直接判断;

该算法复杂度为 (O(mlog^2 n))

但是这玩意还可以进一步优化,注意到 (u) 是分裂节点的必要条件是 (u) 有两个或更多子树。

后缀树上有两个或更多子树的串只有 (O(n)) 个,可以考虑用字符串哈希预处理其对应的节点。

二分的时候如果在哈希表中难以找到 ([l,mid]) 这个串,就说明该串在树边上而非节点上,一定不会成为分裂节点。

那么只需要二分找到最大的 (mid) ,容斥掉的点数即为 (mid-l+1)

该算法复杂度为 (O(mlog n))

#include<bits/stdc++.h>
using namespace std;

const int N=200005;

int n,q,a[N],out[300005],tot=1,lst=1,fa[N],len[N],kp[N],pos[N],u[N],f[N];
map<int,int>ch[N];
vector<int>Edge[N];

void upd(int x,int y){for(;x;x&=x-1)u[x]+=y;}
int query(int x){int f=0;for(;x<=n;x+=x&-x)f+=u[x];return f;}

void Extend(int o)
{
	int p=lst,np=++tot;
	lst=np;
	len[np]=len[p]+1;
	for(;p&&!ch[p][o];p=fa[p])ch[p][o]=np;
	if(!p)
		fa[np]=1;
	else
	{
		int q=ch[p][o];
		if(len[p]+1==len[q])
			fa[np]=q;
		else
		{
			int nq=++tot;
			len[nq]=len[p]+1;
			fa[nq]=fa[q];ch[nq]=ch[q];kp[nq]=kp[q];
			fa[q]=fa[np]=nq;
			for(;p&&ch[p][o]==q;p=fa[p])ch[p][o]=nq;
		}
	}
}

struct hastmap{
	int te,h[1<<20],nxt[N],to[N];
	uint64_t fr[N];
	void emplace(uint64_t x,int y){
		nxt[++te]=h[x&1048575];
		h[x&1048575]=te;
		fr[te]=x;
		to[te]=y;
	}
	int operator[](uint64_t x){
		for(int i=h[x&1048575];i;i=nxt[i])
			if(fr[i]==x)return to[i];
		return 0;
	}
}st;

uint64_t hs[N],w[N];

uint64_t gethash(int l,int r)
{return hs[r]-hs[l-1]*w[r-l+1];}

void dfs(int u){
	if(u!=1){
		int r=kp[u],l=kp[u]-len[u]+1;
		st.emplace(gethash(l,r),u);
	}
	for(int &v:Edge[u])dfs(v);
}

void init(){
	for(int i=w[0]=1; i<=n; i++){
		w[i]=w[i-1]*19491001;
		hs[i]=hs[i-1]*19491001+a[i];
	}dfs(1);
}

struct lct{
	int fa[N],ch[N][2],cov[N],c[N],stk[N],top;
	#define lc(x) ch[x][0]
	#define rc(x) ch[x][1]
	bool Z(int x){return rc(fa[x])==x;}
	bool nrt(int x){return lc(fa[x])==x||rc(fa[x])==x;}
	void rot(int x){
		int y=fa[x],v=Z(x);
		if(nrt(y))ch[fa[y]][Z(y)]=x;
		fa[x]=fa[y];
		fa[ch[y][v]=ch[x][!v]]=y;
		fa[ch[x][!v]=y]=x;
	}
	void C(int x,int y){cov[x]=c[x]=y;}
	void pushdown(int x){
		if(cov[x])
			C(lc(x),cov[x]),C(rc(x),cov[x]),cov[x]=0;
	}
	void splay(int x){
		stk[top=1]=x;
		for(int z=x;nrt(z);z=fa[z])stk[++top]=fa[z];
		for(;top;pushdown(stk[top--]));
		for(int y;y=fa[x],nrt(x);rot(x))
			if(nrt(y))rot(Z(x)^Z(y)?x:y);
	}
	void access(int x,int i){
		int y;
		for(y=0;x;y=x,x=fa[x]){
			splay(x);
			if(rc(x)&&x!=1&&c[x]){
				if(f[x])upd(f[x],-1);
				upd(f[x]=c[x]-len[x],1);
			}
			rc(x)=y;
		}
		C(y,i);
	}
}t;

struct ask{
	int l,id;
}; vector<ask>v[N];

int main()
{
	scanf("%d%d",&n,&q);
	for(int i=1; i<=n; ++i)scanf("%d",a+n-i+1);
	for(int i=1; i<=n; ++i)Extend(++a[i]),kp[pos[i]=lst]=i;
	for(int i=2; i<=tot; ++i){
		t.fa[i]=fa[i];
		Edge[fa[i]].push_back(i);
	}
	init();
	for(int l,r,i=0; i<q; ++i){
		scanf("%d%d",&l,&r);
		v[n-l+1].push_back((ask){n-r+1,i});
	}
	for(int i=1; i<=n; ++i){
		t.access(pos[i],i);
		for(auto u:v[i]){
			out[u.id]=query(u.l)+i-u.l+1;
			int l=u.l,r=i-1,p=0;
			while(l<=r){
				int mid=(l+r)>>1;
				int t=st[gethash(u.l,mid)];
				if(!t||f[t]+len[t]<=mid)r=mid-1;
				else p=mid-u.l+1,l=mid+1;
			}
			out[u.id]-=p;
		}
	}
	for(int i=0;i<q;++i)printf("%d
",out[i]);
}

以上是关于[补档题解]后缀树节点数的主要内容,如果未能解决你的问题,请参考以下文章

[补档计划] 后缀数组

证明二叉树节点数公式

算法总结:左神—利用二分查找思想:完全二叉树节点数,求一个整数k的N次方

[补档计划] SAM

非递归算法求二叉树叶子节点数

囤题 [补档]