洛谷 - P6292 區間本質不同子串個數(SAM+LCT+線段樹)

Frozen_Guardian發表於2020-10-30

題目連結:點選檢視

題目大意:給出一個長度為 n 的字串,再給出 m 次詢問,每次詢問需要回答區間 [ l , r ] 內有多少個本質不同的字串

題目分析:首先簡化模型,回顧一下如何求解 “區間內有多少個不同的數” :SPOJ - DQUERY

求解上面那道題目的方法可謂是五花八門,我們採用其中一種離線的方式:先將所有的詢問儲存下來,按照右端點 r 排序後,每次對於重複出現的數字,用線段樹維護其最後一次出現的位置,那麼答案就是 [ l , r ] 中元素的個數了

對於這個題目而言,將每個本質不同的字串視為一個連續的區間 [ l , r ],我們只需要維護左端點最後一次出現的位置即可

因為需要知道每個子串最後一次出現的位置,所以我們選擇對字串構造字尾自動機,對於某個右端點 r 來說,對應到 SAM 上,從最長的那個字首 [ 1 , r ] 對應的節點開始,沿著 parent 樹到根節點的這條路徑上,都是以 r 為右端點的子串,設 pre[ i ] 為節點 i 上一次出現時的右端點位置,我們只需要暴跳 father,每次將之前出現過的位置,線上段樹上區間更新成 -1 (因為對於每個節點來說,代表的都是一段連續的字尾,所以可以用到線段樹的區間更新),最後再將 [ 1 , r ] , [ 2 , r ] ... [ r , r ] 這 r 個字尾所代表的子串的左端點,即 [ 1 , r ] 線上段樹上 +1 就好了

不過問題是,這樣暴跳 father 的時間複雜度是不正確的,直接這樣實現在本題中只能得到 80 分,有一個測試點會超時

考慮優化,因為每次選擇一條鏈,自下而上去更新,其本質就是:

  1. 令這條鏈每個節點相應的位置線上段樹上區間更新
  2. 令每個節點都被端點 r 覆蓋(也就是將 pre 都標記為 r)

這個過程其實就是 LCT 中的 access 函式的操作,這樣每次更新至多需要更新 logn 個 splay,套上線段樹就是 log^2n,均攤下來就是 nlog^2n 了

因為在 SAM 自底向上的一條路徑上,其中任意連續的一段,所能表示的字尾也是連續的,所以在 LCT 的 splay 上只需要維護一下相應重鏈上最短的子串長度就好了

因為在本題中用不到 LCT 的 link、cut 等操作,所以將相應的一些基本函式都刪掉了,只保留了與 access 函式有關的一些基本函式

總時間複雜度是 O( nlog^2n + mlogn )

程式碼:
 

//#pragma GCC optimize(2)
//#pragma GCC optimize("Ofast","inline","-ffast-math")
//#pragma GCC target("avx,sse2,sse3,sse4,mmx")
#include<iostream>
#include<cstdio>
#include<string>
#include<ctime>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<stack>
#include<climits>
#include<queue>
#include<map>
#include<set>
#include<sstream>
#include<cassert>
#include<bitset>
using namespace std;
     
typedef long long LL;
     
typedef unsigned long long ull;
     
const int inf=0x3f3f3f3f;

const int N=1e6+100;

char s[N];

vector<pair<int,int>>q[N];

LL ans[N];

namespace Seg
{
	struct Node
	{
		int l,r,len;
		LL sum,lazy;
	}tree[N<<2];
	void pushup(int k)
	{
		tree[k].sum=tree[k<<1].sum+tree[k<<1|1].sum;
	}
	void pushdown(int k)
	{
		if(tree[k].lazy)
		{
			LL lz=tree[k].lazy;
			tree[k].lazy=0;
			tree[k<<1].sum+=tree[k<<1].len*lz;
			tree[k<<1|1].sum+=tree[k<<1|1].len*lz;
			tree[k<<1].lazy+=lz;
			tree[k<<1|1].lazy+=lz;
		}
	}
	void build(int k,int l,int r)
	{
		tree[k].l=l;
		tree[k].r=r;
		tree[k].len=r-l+1;
		tree[k].sum=tree[k].lazy=0;
		if(l==r)
			return;
		int mid=l+r>>1;
		build(k<<1,l,mid);
		build(k<<1|1,mid+1,r);
	}
	void update(int k,int l,int r,LL val)
	{
		if(tree[k].l>r||tree[k].r<l)
			return;
		if(tree[k].r<=r&&tree[k].l>=l)
		{
			tree[k].sum+=tree[k].len*val;
			tree[k].lazy+=val;
			return;
		}
		pushdown(k);
		update(k<<1,l,r,val);
		update(k<<1|1,l,r,val);
		pushup(k);
	}
	LL query(int k,int l,int r)
	{
		if(tree[k].l>r||tree[k].r<l)
			return 0;
		if(tree[k].l>=l&&tree[k].r<=r)
			return tree[k].sum;
		pushdown(k);
		return query(k<<1,l,r)+query(k<<1|1,l,r);
	}
}

namespace SAM
{
	int tot,last,pos[N],pre[N];
	struct Node
	{
	    int ch[26];
	    int fa,len;
	}st[N<<1];
	inline int newnode()
	{
		tot++;
		for(int i=0;i<26;i++)
			st[tot].ch[i]=0;
		st[tot].fa=st[tot].len=0;
		return tot;
	}
	void add(int x)
	{
	    int p=last,np=last=newnode();
	    st[np].len=st[p].len+1;
	    while(p&&!st[p].ch[x])st[p].ch[x]=np,p=st[p].fa;
	    if(!p)st[np].fa=1;
	    else
	    {
	        int q=st[p].ch[x];
	        if(st[p].len+1==st[q].len)st[np].fa=q;
	        else
	        {
	            int nq=newnode();
	            st[nq]=st[q]; st[nq].len=st[p].len+1;
	            st[q].fa=st[np].fa=nq;
	            while(p&&st[p].ch[x]==q)st[p].ch[x]=nq,p=st[p].fa;//向上把所有q都替換成nq
	        }
	    }
	}
	void init()
	{
		last=1;
		tot=0;
		newnode();
	}
	void build()
	{
		init();
		int len=strlen(s+1);
		for(int i=1;i<=len;i++)
		{
			add(s[i]-'a');
			pos[i]=last;
		}
	}
}

namespace LCT
{
	int fa[N],ch[N][2],pre[N],min_len[N],len[N],Stack[N],lazy[N],top;
	bool son(int x)//判斷點x是父節點的左兒子還是右兒子 
	{
		return x==ch[fa[x]][1];
	}
	bool isroot(int x)//判斷一個點是不是根節點(當前splay的根節點)
	{
		return ch[fa[x]][0]!=x&&ch[fa[x]][1]!=x;
	}
	void pushup(int x)//上傳(維護資訊)
	{
		min_len[x]=min(len[x],min(min_len[ch[x][0]],min_len[ch[x][1]]));
	}
	void update_lazy(int x,int lz)
	{
		pre[x]=lazy[x]=lz;
	}
	void pushdown(int x)//下傳(更新反轉標記用)
	{
		if(lazy[x])
		{
			update_lazy(ch[x][0],lazy[x]);
			update_lazy(ch[x][1],lazy[x]);
			lazy[x]=0;
		}
	}
	void rotate(int x)//splay的旋轉 
	{
		int y=fa[x],z=fa[y],c=son(x);
		ch[y][c]=ch[x][c^1];if(ch[y][c]) fa[ch[y][c]]=y;
		fa[x]=z;if(!isroot(y))ch[z][son(y)]=x;
		ch[x][c^1]=y;fa[y]=x;pushup(y);
	}
	void splay(int x)//將x轉到根節點 
	{
		Stack[top=1]=x;
		for (int i=x;!isroot(i);i=fa[i])
			Stack[++top]=fa[i];
		while (top) pushdown(Stack[top--]);
		for (int y=fa[x];!isroot(x);rotate(x),y=fa[x])
			if (!isroot(y)) son(x)^son(y)?rotate(x):rotate(y);
		pushup(x);
	}
	void access(int x,int id)//把x節點到x所在樹(連通塊)的根節點之間的路徑全部變成重路徑
	{
		int y;
		for(y=0;x;y=x,x=fa[x])
		{
			splay(x),ch[x][1]=y,pushup(x);
			if(pre[x])
				Seg::update(1,pre[x]-SAM::st[x].len+1,pre[x]-min_len[x]+1,-1);
		}
		splay(y);
		update_lazy(y,id);
		Seg::update(1,1,id,1);
	}
	void build()
	{
		min_len[0]=inf;
		for(int i=1;i<=SAM::tot;i++)
		{
			fa[i]=SAM::st[i].fa;
			len[i]=min_len[i]=SAM::st[SAM::st[i].fa].len+1;
			lazy[i]=ch[i][0]=ch[i][1]=pre[i]=0;
		}
	}
};

int main()
{
#ifndef ONLINE_JUDGE
//  freopen("data.in.txt","r",stdin);
//  freopen("data.out.txt","w",stdout);
#endif
//  ios::sync_with_stdio(false);
	scanf("%s",s+1);
	int n=strlen(s+1);
	SAM::build();
	LCT::build();
	Seg::build(1,1,n);
	int m;
	scanf("%d",&m);
	for(int i=1;i<=m;i++)
	{
		int l,r;
		scanf("%d%d",&l,&r);
		q[r].emplace_back(i,l);
	}
	for(int i=1;i<=n;i++)
	{
		LCT::access(SAM::pos[i],i);
		for(auto it:q[i])
			ans[it.first]=Seg::query(1,it.second,i);
	}
	for(int i=1;i<=m;i++)
		printf("%lld\n",ans[i]);













    return 0;
}

 

相關文章