[補檔題解]后綴樹節點數


題目描述

給定一個長度為 \(n\) 的字符串 \(P\),有 \(m\) 次詢問,每次給定兩個參數 \(l\) , \(r\),詢問子串 \(P[l,r]\) 所構成的后綴樹的結點數。

\(n\le 10^5,m\le 3\times 10^5\)

題解

tag:分類計數;后綴樹/后綴自動機;線段樹/樹狀數組;哈希。

做法來自2018集訓隊論文集。

由於構建后綴樹需要倒轉原串之后建立 SAM 得到,為方便處理,故我們先倒轉原串以及對應詢問區間。

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

\(node_{[l,r]}\) 表示找區間 \([l,r]\) 對應子串在后綴樹上的節點。

考慮傳統后綴樹節點數的計數方法,對於一個區間 \([l,r]\) 建后綴自動機的過程,產生出了 np 和 nq 兩種類型的節點。

np 節點對應的是 \(i\in [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_u\ge l\)\(pos_b-len_u\ge l\),那么 \(u\) 對應的節點將在 \([l,r]\) 中作為分裂節點出現。

這個是個傳統樹上數據結構題,一種好寫的方法是 lct 維護后綴樹,access 更新過程中從 \(root\)\(node_{[1,r]}\) 的路徑上染上 \(r\) 這種顏色,再維護每個節點來自不同子樹最后兩次染上的顏色分別是 \(lst_u\)\(now_u\),然后再用樹狀數組維護樹上所有節點的 \(lst_u-len_u\),這樣查詢樹狀數組上 \(\ge l\) 的部分的和就是分裂節點的個數。

這一段時間復雜度是 \(O(n\log^2 n+m\log n)\),來自於 lct 和樹狀數組。

接下來還需要計算前綴&分裂節點的個數。

考慮一種可行的暴力,枚舉所有 \(i\in [l,r]\) 找到 \(node_{[l,i]}\) 的位置 \(p\)(※),然后再判斷該位置是否有兩個或以上子樹,然后再判斷是否 \(lst_p\gt mid\),僅當全部為“是”就對答案帶來 \(-1\) 的貢獻。

(※)筆者注:這個位置不一定恰好是樹上的點,可能在邊上,在邊上顯然不滿足有兩個或以上子樹。這個位置就是對應的前綴節點將產生的位置,也就意味着,對 \([l,r]\) 這個區間建后綴樹,所有的分裂節點一定是整個長串的后綴樹上節點的子集,而 \(r-l+1\) 個前綴節點不一定全是。

接下來將該暴力優化至 \(O(m\log n)\) 的復雜度:

引理:所有可行的 \(i\in [l,r]\) 是一段前綴。

證明:顯然一個串 \([l,i]\) 能找到兩個出現位置,其往前一個位置的字符不同,則 \([l,i-1]\) 也可以。

example:"abcdabceabc"

其中 "abc" 作為分裂&前綴節點出現,可以直接說明前三個前綴節點均算重了。

有了引理,就可以上一個二分,每次 check \([l,mid]\) 這個串是否形成分裂節點即可。

在后綴自動機上找 \(node_{[l,mid]}\) ,傳統方法是利用倍增預處理,單次以 \(O(\log n)\) 的復雜度查詢;

判定分裂節點可以直接用上一部分算出的 \(lst_u\) 來直接判斷;

該算法復雜度為 \(O(m\log^2 n)\)

但是這玩意還可以進一步優化,注意到 \(u\) 是分裂節點的必要條件是 \(u\) 有兩個或更多子樹。

后綴樹上有兩個或更多子樹的串只有 \(O(n)\) 個,可以考慮用字符串哈希預處理其對應的節點。

二分的時候如果在哈希表中難以找到 \([l,mid]\) 這個串,就說明該串在樹邊上而非節點上,一定不會成為分裂節點。

那么只需要二分找到最大的 \(mid\) ,容斥掉的點數即為 \(mid-l+1\)

該算法復雜度為 \(O(m\log 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\n",out[i]);
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM