2019.02.26【九省联考2018】【BZOJ5253】【洛谷P4384】制胡窜(后缀自动机SAM)(树上倍增)(线段树合并)

BZOJ传送门

洛谷传送门


解析:

神题啊。。。看了shadowice的题解,不过他有的地方写错了,让我困惑了好一阵,所以这里再写一篇题解。不过基本思路和叙述都和shadowice差不多,修改了一些他写错了的地方。

题目要你求一个东西:给出一个字符串和它的某一个子串,问在原串上切两刀(不重复),使得切出来的三个串中至少有一个包含一个该子串,有多少种方案?

正难则反,考虑它的对偶问题,切出来的三个子串中没有一个包含询问子串。

显然总的方案数始终是 ( n − 1 2 ) {n-1\choose 2} (2n1),只需要减一减就行了。接下来所有方案数均指该转化后的问题的方案数。

首先我们需要知道一个子串所有在原串中出现的位置。

其实就是后缀自动机匹配位置对应的 r i g h t right right集合,即 e n d p o s endpos endpos集合(这篇博客可能有地方会混用,其实这两个名词说的是一个东西)。

显然这道题数据规模,不可能直接暴力求出 r i g h t right right集合。

由于允许离线,现在我们考虑一个一般的东西:线段树合并。

当然,由于暂时不知道需要维护些什么,所以这个玩意先放到一边。

现在考虑一个很蛋疼的问题:怎么知道该子串的第一个匹配位置?

。。。

暴力跑?祝您好运。

现在已经知道是原串的一个子串了啊,有没有什么更加高效的方法?

显然我们知道串 S l , r S_{l,r} Sl,r一定有一个匹配右端点是 r r r

如果你对后缀自动机那套理论相当熟悉的话现在应该已经反应过来了。

子串的第一个匹配节点的性质:

  1. r i g h t right right集合中包含该子串的右端点,即该匹配节点在该右端点对应节点的 f a i l fail fail链上
  2. f a i l fail fail树上的任何祖先都是该串的后缀,换句话说,匹配节点的任意祖先字符串长度小于该子串长度。

综上,匹配节点是右端点 f a i l fail fail链上最浅的能够完整包含该子串的节点。

树上倍增解决一切。

现在考虑我们get到了一个匹配位置的 e n d p o s endpos endpos集合,怎么求方案数。

一下假设 l p lp lp是所有 e n d p o s endpos endpos中的最小值, l e n len len是询问子串的长度, e d ed ed是该子串所有匹配的开头中最小的,即 e n d p o s endpos endpos中的最大值 − l e n + 1 -len+1 len+1

我们的目标是两刀切完所有的询问串,开始分析。

一、出现了三个两两不相交的子串

怎么切都gg,由抽屉原理可以知道,方案数为 0 0 0

只需要询问一下 e d ed ed之前出现的最大 e n d p o s endpos endpos,将该匹配位置的开头与 l p lp lp比较一下就行了。

线段树维护区间最大值,用于查前驱。

二、没有三个子串两两不相交

这种情况的分类讨论相当复杂,慢慢分析。

1.最左串与最右串相交

l p ≥ e d lp\geq ed lped

注意到这种情况所有的串的都是相交的。

仍然不好一下讨论,继续对两刀的情况分类。

1.2第一刀没有切完所有子串

注意到有两个性质:

  • 确定第一刀的位置之后,合法的第二刀位置是一个连续的区间。
  • 使得合法的第二刀位置集合相同的第一刀的位置,是一个连续的区间。

证明相当好想,考虑我们第二刀的目的是什么,就是将所有剩下的没有分割的子串全部分割。

显然确定第一刀的位置之后,剩下的线段的交集是一个连续区间。

而当剩下的线段集合确定之后,补集(也就是第一刀需要切断的子串集合)也是确定的,交集显然是一个连续的区间。

r t i rt_i rti是第 i i i小的 e n d p o s endpos endpos所以由乘法分配率直接可以得到这一部分的答案就是:
∑ i ( ( r t i + 1 − l e n + 1 ) − ( r t i − l e n + 1 ) ) ( r t i + 1 − e d ) \sum_{i}((rt_{i+1}-len+1)-(rt_{i}-len+1))(rt_{i+1}-ed) i((rti+1len+1)(rtilen+1))(rti+1ed)

每一项算的是这个东西:第一刀切断前 i i i个串之后,第二刀切断 i + 1 i+1 i+1及之后所有的串。

等价于:

∑ i ( r t i + 1 − r t i ) r t i + 1 − e d ∑ i ( r t i + 1 − r t i ) \sum_{i}(rt_{i+1}-rt_{i})rt_{i+1}-ed\sum_{i}(rt_{i+1}-rt_{i}) i(rti+1rti)rti+1edi(rti+1rti)

线段树维护一下这两项就行了(顺便维护最大最小值):

∑ i ( r t i + 1 − r t i ) r t i + 1 ∑ i r t i + 1 − r t i \sum_{i}(rt_{i+1}-rt_{i})rt_{i+1}\\\sum_{i}rt_{i+1}-rt_{i} i(rti+1rti)rti+1irti+1rti

注意这里并没有限制第二刀不能切完所有子串。

1.2第一刀切完了所有子串

显然第一刀落在了 ( l p , e d ) (lp,ed) (lp,ed)位置当中,首先考虑第二刀也在这个区间就有 ( l p − e d 2 ) {lp-ed\choose 2} (2lped)种方案。

然后考虑第二刀在剩下的什么地方不会与上一个讨论形成重复。

显然,如果第二刀切断了前面的一部分子串(不是全部),就会和前面的方案形成重复,所以第二刀的合法区间是 ( 1 , l p − l e n ) (1,lp-len) (1,lplen) ( l p + 1 , n ) (lp+1,n) (lp+1,n)

这一部分的答案也就是 ( l p − e d 2 ) + ( n − l e n ) ∗ ( l p − e d ) {lp-ed\choose 2}+(n-len)*(lp-ed) (2lped)+(nlen)(lped)

于是情况1的答案就是上面两个式子的和。

2.最左串和最右串并不相交

l p &lt; e d lp &lt; ed lp<ed

由于前面已经排除了三个串两两不相交的情况,所以这里只有三种情况:

  1. 只和最左串相交
  2. 只和最右串相交
  3. 和两个串都相交

现在考虑效仿前面1.1里面切断两个集合交集的分析,我们仍然可以得到一个和上面长得很像的式子:

∑ i ( ( r t i + 1 − l e n + 1 ) − ( r t i − l e n + 1 ) ) ( r t i + 1 − e d ) \sum_{i}((rt_{i+1}-len+1)-(rt_{i}-len+1))(rt_{i+1}-ed) i((rti+1len+1)(rtilen+1))(rti+1ed)

但是自己手推一遍都应该明白,这里有限制改变了。

计入答案的第一项 r t 1 rt_1 rt1应该是 e d ed ed r i g h t right right集合里面的前驱,因为所有合法的 r t i + 1 rt_{i+1} rti+1显然需要在 e d ed ed后面。

限制就是这个 r t i + 1 &gt; e d , r t i + 1 − l e n + 1 &lt; l p rt_{i+1} &gt; ed ,rt_{i+1}-len+1 &lt; lp rti+1>ed,rti+1len+1<lp

线段树区间询问就行了。。。

本题最容易忽视的一个细节来了,这种情况的第一刀在碰到 l p lp lp的时候停下来,而不是某一个串的左端点。而在上一种情况中,最后一个碰到的是 e d ed ed这个左端点,下一刀就可以直接切断所有子串了。

所以我们需要修正。

e n d p o s endpos endpos中找到 l p + l e n − 1 lp+len-1 lp+len1的前驱后继,记为 p r e , s u f pre,suf pre,suf

我们发现,第一道切在 ( p r e − l e n + 1 , l p ) (pre-len+1,lp) (prelen+1,lp)的时候,第二刀只能在 ( e d , s u f ) (ed,suf) (ed,suf)中取到。

所以修正的值就是: ( l p − p r e + l e n − 1 ) ( s u f − e d ) [ s u f &gt; e d ] (lp-pre+len-1)(suf-ed)[suf &gt; ed] (lppre+len1)(sufed)[suf>ed]

然后离线处理,向上跳的同时线段树合并维护信息,这道题就做完了


代码(改了一点小trick,luogu机子开O2会RE):

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define re register
#define gc get_char
#define cs const

namespace IO{
	inline char get_char(){
		static cs int Rlen=1<<20|1;
		static char buf[Rlen],*p1,*p2;
		return (p1==p2)&&(p2=(p1=buf)+fread(buf,1,Rlen,stdin),p1==p2)?EOF:*p1++;
	}
	
	inline int getint(){
		re char c;
		while(!isdigit(c=gc()));re int num=c^48;
		while(isdigit(c=gc()))num=(num+(num<<2)<<1)+(c^48);
		return num;
	}
}
using namespace IO;

cs int N=1e5+5,logN=20;
cs int INF=0x3f3f3f3f;
int n,m;
vector<int> edge[N<<1];
namespace SAM{
	int son[N<<1][10],fa[N<<1],len[N<<1];
	int now=1;
	
	inline void init(){now=n+1;}
	
	inline void push_back(int i,char c){
		c-='0';
		int cur=i,p=(i-1)?(i-1):(n+1);
		len[cur]=len[p]+1;
		for(;p&&!son[p][c];p=fa[p])son[p][c]=cur;
		if(!p)fa[cur]=n+1;
		else if(len[son[p][c]]==len[p]+1)fa[cur]=son[p][c];
		else {
			int clone=++now,q=son[p][c];
			memcpy(son[clone],son[q],sizeof son[q]);
			len[clone]=len[p]+1;
			fa[clone]=fa[q];
			fa[q]=fa[cur]=clone;
			for(;p&&son[p][c]==q;p=fa[p])son[p][c]=clone;
		}
	}
	
	inline void build(){for(int re i=1;i<=now;++i)edge[fa[i]].push_back(i);}
}
using SAM::len;

int fa[N<<1][logN+2];
void dfs1(int u){
	for(int re i=0;fa[u][i];++i)fa[u][i+1]=fa[fa[u][i]][i];
	for(int re e=0;e<edge[u].size();++e)fa[edge[u][e]][0]=u,dfs1(edge[u][e]);
}

inline int find(int u,int l){
	for(int re i=logN;~i;--i)if(fa[u][i]&&len[fa[u][i]]>=l)u=fa[u][i];
	return u;
}

struct Query{int id,len;};
vector<Query> vec[N<<1];

namespace SGT{
	int mn[N*(logN+2)],mx[N*(logN+2)];
	ll v1[N*(logN+2)],v2[N*(logN+2)];
	int lc[N*(logN+2)],rc[N*(logN+2)];
	int tot,right;
	ll ans1,ans2;
	
	inline void init(){tot=SAM::now;}
	
	inline void pushup(int k){
		int l=lc[k],r=rc[k];
		if(!l){mn[k]=mn[r],mx[k]=mx[r],v1[k]=v1[r],v2[k]=v2[r];return;}
		if(!r){mn[k]=mn[l],mx[k]=mx[l],v1[k]=v1[l],v2[k]=v2[l];return;}
		mn[k]=mn[l],mx[k]=mx[r];
		ll delta=mn[r]-mx[l];
		v1[k]=v1[l]+v1[r]+delta*mn[r];
		v2[k]=v2[l]+v2[r]+delta;
	}
	
	inline int merge(int u,int v){
		if(lc[u]&&lc[v])merge(lc[u],lc[v]);else if(!lc[u])lc[u]=lc[v];
		if(rc[u]&&rc[v])merge(rc[u],rc[v]);else if(!rc[u])rc[u]=rc[v];
		pushup(u);
	}
	
	inline void ins(int k,int l,int r,int pos){
		mx[k]=mn[k]=pos;if(l==r)return ;
		int mid=(l+r)>>1;
		if(pos<=mid)ins(lc[k]=++tot,l,mid,pos);
		else ins(rc[k]=++tot,mid+1,r,pos);
	}
	
	inline int query_min(int k,int l,int r,cs int &ql,cs int &qr){
		if(ql<=l&&r<=qr)return mn[k];
		int mid=(l+r)>>1;
		int res=INF;
		if(lc[k]&&ql<=mid)res=query_min(lc[k],l,mid,ql,qr);
		if(res!=INF)return res;
		if(rc[k]&&mid<qr)return query_min(rc[k],mid+1,r,ql,qr);
		return INF;
	}
	
	inline int query_max(int k,int l,int r,cs int &ql,cs int &qr){
		if(ql<=l&&r<=qr)return mx[k];
		int mid=(l+r)>>1;
		int res=-INF;
		if(rc[k]&&mid<qr)res=query_max(rc[k],mid+1,r,ql,qr);
		if(res!=-INF)return res;
		if(lc[k]&&ql<=mid)return query_max(lc[k],l,mid,ql,qr);
		return -INF;
	}
	
	inline void query(int k,int l,int r,cs int &ql,cs int &qr){
		if(ql<=l&&r<=qr){
			ll delta=mn[k]-right;
			right=mx[k];
			ans1+=v1[k]+delta*mn[k];
			ans2+=v2[k]+delta;
			return ;
		}
		int mid=(l+r)>>1;
		if(lc[k]&&ql<=mid)query(lc[k],l,mid,ql,qr);
		if(rc[k]&&mid<qr)query(rc[k],mid+1,r,ql,qr);
	}
	
	inline ll qry(int k,int l,int r,int be,int ed){
		right=be;
		ans1=0;
		ans2=0;
		query(k,1,n,l,r);
		return ans1-ed*ans2;
	}
	inline ll calc(int k,int ed){return v1[k]-ed*v2[k];}
}
using SGT::query_min;
using SGT::query_max;
using SGT::qry;
using SGT::calc;

inline ll c_2(ll n){return n*(n-1)>>1;}
ll ans[N*3];

void dfs2(int u){
	for(int re e=0;e<edge[u].size();++e){
		dfs2(edge[u][e]);
		SGT::merge(u,edge[u][e]);
	}
	int lp=SGT::mn[u],rp=SGT::mx[u];
	for(vector<Query>::iterator it=vec[u].begin();it!=vec[u].end();++it){
		int ed=rp-it->len+1;
		if(ed<=lp)ans[it->id]=calc(u,ed)+c_2(lp-ed)+(ll)(lp-ed)*(n-it->len);
		else {
			int pos=query_max(u,1,n,1,ed);
			if(lp+it->len-1<=pos)continue;
			ans[it->id]+=qry(u,ed+1,lp+it->len-1,pos,ed);
			int pos1=query_min(u,1,n,lp+it->len,n);pos=query_max(u,1,n,1,lp+it->len-1);
			if(pos!=-INF&&pos1!=INF)if(pos1>ed)ans[it->id]+=(ll)(lp-pos+it->len-1)*(pos1-ed);
		}
	}
}

char s[N];
signed main(){
	scanf("%d%d",&n,&m);
	scanf("%s",s+1);
	SAM::init();
	for(int re i=1;i<=n;++i)SAM::push_back(i,s[i]);
	SAM::build();
	dfs1(n+1);
	SGT::init();
	for(int re i=1,l,r;i<=m;++i){
		l=getint(),r=getint();
		vec[find(r,r-l+1)].push_back((Query){i,r-l+1});
	}
	for(int re i=1;i<=n;++i)SGT::ins(i,1,n,i);
	dfs2(n+1);
	ll tmp=c_2(n-1);
	for(int re i=1;i<=m;++i)cout<<tmp-ans[i]<<"\n";
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值