1. 程式人生 > 實用技巧 >【LOJ6031】「雅禮集訓 2017 Day1」字串(字尾自動機+設閾值)

【LOJ6031】「雅禮集訓 2017 Day1」字串(字尾自動機+設閾值)

點此看題面

  • 給定一個長度為\(n\)的字串\(s\)\(m\)個區間。
  • \(q\)次詢問,每次給定一個長度為\(k\)的字串\(w_i\)和兩個數\(l_i,r_i\),求第\(l_i\sim r_i\)個區間在\(w_i\)中對應的子串在\(s\)中出現次數之和。
  • \(n,m,q\times k\le10^5\)

設閾值/分類討論

首先對於這種問題容易想到對於給定串\(s\)建一個字尾自動機。

然後發現由於題目中給的是\(q\times k\)的限制,感覺很難給出一個通解。

因此我們考慮設閾值,其實這道題中也就是根據\(q,k\)的大小關係分類討論。

每塊分類討論中會給出具體的複雜度,最終程式碼的複雜度表示就籠統地將\(n,m,qk\)

視為同階的\(N\)了。

\(k\le q\)的暴力查詢

考慮我們列舉\(w\)中的每一個左端點\(i\),然後每次右移一位右端點\(j\)都相當於是在後綴自動機上向下走了一步。

\([i,j]\)子串在\(s\)中的出現次數實際上就是當前節點在\(parent\)樹上的子樹大小(這顯然可以預處理出來)。

而我們一開始就可以對於每個\([i,j]\)\(vector\)存下等於它的區間編號,那麼每次只要呼叫\(lower\_bound\)\(upper\_bound\)便能求出有多少為\([i,j]\)的區間在詢問的\([l,r]\)中。

這一部分的複雜度應該是\(O(k^2qlogm)\)

的。

\(k>q\)的倍增求解

考慮我們先對於\(w\)中的每個位置,求出以它為右端點能得到的最長字尾在後綴自動機中的對應節點及其長度,這隻要在後綴自動機上走一遍就好了,和\(AC\)自動機完全一樣。(可以看程式碼,應該非常顯然)

然後列舉\(l\sim r\)的區間\([L_i,R_i]\),先判斷如果\(R_i\)對應的最長字尾長度小於\(R_i-L_i+1\),說明這個子串壓根沒出現過。

否則,我們在\(parent\)樹上倍增上跳,找到深度最小的長度大於等於\(R_i-L_i+1\)的節點,則這個節點的子樹大小就是子串\([L_i,R_i]\)\(s\)中出現的次數。

這一部分的複雜度應該是\(O(qmlogn)\)

的。

程式碼:\(O(N\sqrt NlogN)\)

#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define N 100000
#define SN 350
#define LN 20
#define LL long long
using namespace std;
int n,m,k,Qt;char s[N+5];struct Q {int l,r;}q[N+5];
class SuffixAutomation//字尾自動機
{
	private:
		int Nt,lst,cur,ct[N+5],q[N<<1];struct node {int Sz,C,L,F[LN+5],S[30];}O[N<<1];
		I void MakeFa(CI x,CI f)//讓x的父節點變成f
		{
			O[x].F[0]=f;for(RI i=1;i<=LN;++i) O[x].F[i]=O[O[x].F[i-1]].F[i-1];//預處理倍增陣列
		}
	public:
		I SuffixAutomation() {Nt=1;}I void Init() {lst=1;}I void Ins(CI x)//插入字元
		{
			RI p=lst,o=lst=++Nt;O[o].L=O[p].L+1,O[o].Sz=1;
			W(p&&!O[p].S[x]) O[p].S[x]=o,p=O[p].F[0];if(!p) return MakeFa(o,1);
			RI q=O[p].S[x];if(O[p].L+1==O[q].L) return MakeFa(o,q);
			RI k=++Nt;(O[k]=O[q]).L=O[p].L+1,O[k].Sz=0,MakeFa(o,k),MakeFa(q,k);
			W(p&&O[p].S[x]==q) O[p].S[x]=k,p=O[p].F[0];
		}
		I void Calc(CI n)//統計子樹大小(基排,或DFS也可)
		{
			RI i;for(i=1;i<=Nt;++i) ++ct[O[i].L];for(i=1;i<=n;++i) ct[i]+=ct[i-1];
			for(i=1;i<=Nt;++i) q[ct[O[i].L]--]=i;for(i=Nt;i^1;--i) O[O[q[i]].F[0]].Sz+=O[q[i]].Sz;
		}
		I void Start() {cur=1;}I int Go(CI x) {return O[cur=O[cur].S[x]].Sz;}//k≤q時在後綴自動機上走路
		I void Add(CI id,CI x)//k>q預處理時加入一個新字元
		{
			!(q[id]=q[id-1])&&(q[id]=1),ct[id]=ct[id-1];//從上個字元繼承資訊
			W(q[id]&&!O[q[id]].S[x]) ct[id]=O[q[id]=O[q[id]].F[0]].L;//如果沒有這個兒子就不斷上跳父節點
			(q[id]=O[q[id]].S[x])?++ct[id]:ct[id]=0;//維護最大長度
		}
		I int Get(CI id,CI x)//k>q時倍增求解
		{
			if(ct[id]<x) return 0;RI p=q[id];//如果長度本就不足直接返回0
			for(RI i=LN;~i;--i) O[O[p].F[i]].L>=x&&(p=O[p].F[i]);return O[p].Sz;//倍增上跳
		}
}S;
vector<int> g[SN+5][SN+5];I void SolveK()//k≤q
{
	RI i,j,l,r,tmp;LL t;for(i=1;i<=m;++i) g[q[i].l][q[i].r].push_back(i);W(Qt--)//存下每種詢問的區間編號
	{
		#define G g[i][j].begin(),g[i][j].end()
		for(scanf("%s%d%d",s+1,&l,&r),++l,++r,t=0,i=1;i<=k;++i)//列舉左端點
			for(S.Start(),j=i;j<=k;++j) t+=1LL*S.Go(s[j]&31)*(upper_bound(G,r)-lower_bound(G,l));//移動右端點
		printf("%lld\n",t);
	}
}
I void SolveQ()//k>q
{
	RI i,l,r;LL t;W(Qt--)
	{
		for(scanf("%s%d%d",s+1,&l,&r),++l,++r,S.Start(),i=1;i<=k;++i) S.Add(i,s[i]&31);//求出每個右端點最長字尾對應的節點及其長度
		for(t=0,i=l;i<=r;++i) t+=S.Get(q[i].r,q[i].r-q[i].l+1);printf("%lld\n",t);//每個詢問在parent樹上倍增
	}
}
int main()
{
	RI i;for(scanf("%d%d%d%d%s",&n,&m,&Qt,&k,s+1),S.Init(),i=1;i<=n;++i) S.Ins(s[i]&31);//建字尾自動機
	for(i=1;i<=m;++i) scanf("%d%d",&q[i].l,&q[i].r),++q[i].l,++q[i].r;//讀入區間
	return S.Calc(n),k<=Qt?SolveK():SolveQ(),0;//分類討論
}