九省联考2018 制胡窜

制胡窜

对于一个字符串 \(S\),我们定义 \(|S|\) 表示 \(S\) 的长度。

接着,我们定义 \(S_i\) 表示 \(S\) 中第 \(i\) 个字符,\(S_{L,R}\) 表示由 \(S\) 中从左往右数,第 \(L\) 个字符到第 \(R\) 个字符依次连接形成的字符串。特别的,如果 \(L > R\) ,或者 \(L < [1, |S|]\), 或者 \(R < [1, |S|]\) 我们可以认为 \(S_{L,R}\) 为空串。

给定一个长度为 \(n\) 的仅由数字构成的字符串 \(S\),现在有 \(q\) 次询问,第 \(k\) 次询问会给出 \(S\) 的一个字符串 \(S_{l,r}\) ,请你求出有多少对 \((i, j)\),满足 \(1 \le i < j \le n\)\(i + 1 \lt j\),且 \(S_{l,r}\) 出现在 \(S_{1,i}\) 中或 \(S_{i+1, j−1}\) 中或 \(S_{j,n}\) 中。

对于所有测试数据,\(1 \le n \le 10^5\)\(1 \le q \le 3 · 10^5\)\(1 \le l \le r \le n\)

题解

参照cz_xuyixuanTS_Hugh的题解。

问题跟所有子串有关,考虑后缀自动机。

问题转化

题目中两个“或”已经说明了正面求很难做。正难则反,计算\(i,j\)分布使得三段都不包含所有出现的串的方案数。显然\((i,i+1),(j-1,j)\)这两个空隙要切断所有的串。所以问题转化到了空隙上面,把方案数求出来用\(\binom{n-1}2\)减去它就是答案了。

下面的论述为了方便,以右端点代替空隙,即用\(i\)来代替\((i-1,i)\)这个空隙,显然\(i,j\in[2,n],i<j\)

只询问一个串

首先,一个询问的答案只和询问串的在主串中所有出现的位置有关。因此要找出询问串在后缀自动机上的位置。

定位一个询问的串可以在后缀自动机parent树上倍增在\(O(\log n)\)的时间内完成。如果能预处理出right集合那么所有出现的位置就找到了。

考虑用线段树合并来做这件事。现在我们有了一棵维护着所有询问串出现位置的右端点的线段树,考虑如何得到答案。

考虑较靠前的断点切断了哪些字符串。我们需要求出的即是:

\[\sum_{k=ql}^{qr}询问串第一次出现和第k次出现的交∗询问串最后一次出现和第(k+1)次出现的交 \]

其中\([ql,qr]\)\(k\)可能的取值范围。\(ql\)即第\(k+1\)个串与后面的串有交集时,\(k\)的可取最小值;类似的,\(qr\)即第\(k\)和前面的串有交集时,\(k\)的可取最大值。它们可以在线段树上二分得到。

记询问串第\(k\)次出现的左端点和右端点分别为\(L_k\)\(R_k\)
对于询问串第一次出现和第\(k\)次出现的交,它在大部分的情况下为\(L_{k+1}-L_k\),也即\(R_{k+1}-R_k\),只有在\(k=qr\)时,它有可能为\(R_1−L_i+1\)
对于询问串最后一次出现和第\(k+1\)次出现的交,它在大部分情况下为\(R_{k+1}−L_{last}+1\)

还需要考虑一些特殊情况,可能可以用\(i,j\)中的一个切断所有串。为了不重不漏,对\(i\)来讨论,分为两种情况。

  1. \(i\)不切断任意的字符串,那么它一定在询问串第一次出现之前,并且\(j\)要恰好切断所有字符串,也即\(j\)的取值范围是询问串所有出现位置的并。
    只有在\(k=ql\)时,有可能对应这种情况,此时\(ql=0\)
  2. \(i\)切断了所有的字符串,那么\(j\)只需要满足在较靠前的断点之后即可,因此在这种情况下可能的方案数是一个等差数列的各项之和。
    只有在\(k=qr\)时有可能出现这种情况,此时\(qr=last\)

那么,我们可以对\(k=ql\),和\(k=qr\)时特殊处理。对于剩下的情况,也即\(k\in(ql,qr)\)时,我们需要求出

\[\sum^{qr−1}_{k=ql+1}(R_{k+1}−R_k)∗(R_{k+1}−L_{last}+1) \\ =\sum^{qr−1}_{k=ql+1}(R_{k+1}−R_k)∗R_{k+1}−(L_{last}−1)(R_{qr}−R_{ql+1}) \]

对于求和部分,是关于两个相邻位置的信息,我们可以在线段树上额外维护这个信息。那么一个串就做完了。

询问多个串

由于使用线段树合并要求在线的话,合并的时候必须新建节点,所以空间复杂度就错了。所以把询问离线挂在parent树上,询问的时候在parent按从叶子到根的拓扑序来做。这样线段树合并的时候直接用原来的节点就行了。合并时线段树节点按访问量满来计算,最坏复杂度\(O(n\log n)\)

然后这道题就做完了。时间复杂度\(O(n\log n+q\log n)\)

代码

这题给的数字串……写的字母串调了好久。

co int N=2e5;
int n,m;
char s[N];
ll ans[300001];
// Interval Tree
struct node{int min,max;ll sum;};
node operator+(co node&a,co node&b){
	node c=(node){a.min,b.max,a.sum+b.sum};
	if(!c.min) c.min=b.min;
	if(!c.max) c.max=a.max;
	if(a.max&&b.min) c.sum+=(ll)b.min*(b.min-a.max);
	return c;
}
namespace T{
	node t[N*17];
	int tot,lc[N*17],rc[N*17];
	void insert(int&x,int l,int r,int p){
		if(!x) x=++tot;
		if(l==r) return t[x]=(node){l,l,0},void();
		int mid=l+r>>1;
		if(p<=mid) insert(lc[x],l,mid,p);
		else insert(rc[x],mid+1,r,p);
		t[x]=t[lc[x]]+t[rc[x]];
	}
	int merge(int x,int y){
		if(!x||!y) return x+y;
		lc[x]=merge(lc[x],lc[y]),rc[x]=merge(rc[x],rc[y]);
		t[x]=t[lc[x]]+t[rc[x]];
		return x;
	}
	int lower(int x,int l,int r,int p){ // first >=
		if(t[x].min>=p) return t[x].min;
		int mid=l+r>>1;
		if(t[lc[x]].max&&t[lc[x]].max>=p) return lower(lc[x],l,mid,p);
		else return lower(rc[x],mid+1,r,p);
	}
	int upper(int x,int l,int r,int p){ // last <=
		if(t[x].max<=p) return t[x].max;
		int mid=l+r>>1;
		if(t[rc[x]].min&&t[rc[x]].min<=p) return upper(rc[x],mid+1,r,p);
		else return upper(lc[x],l,mid,p);
	}
	node query(int x,int l,int r,int ql,int qr){
		if(ql>qr) return (node){0,0,0};
		if(ql<=l&&r<=qr) return t[x];
		int mid=l+r>>1;
		if(qr<=mid) return query(lc[x],l,mid,ql,qr);
		if(ql>mid) return query(rc[x],mid+1,r,ql,qr);
		return query(lc[x],l,mid,ql,qr)+query(rc[x],mid+1,r,ql,qr);
	}
	ll query(int x,int len){ // represent gap with right vertice
		if(len<=0) return 0;
		int r1=t[x].min,l1=r1-len+1; // len=r-l+1-1
		int rn=t[x].max,ln=rn-len+1;
		int ql=lower(x,1,n,ln);
		if(ql!=r1) ql=upper(x,1,n,ql-1);
		else ql=0;
		int qr=upper(x,1,n,r1+len-1);
		if(ql>qr) return 0;
		ll ans=0;
		if(ql==qr){
			assert(0<ql&&ql<rn);
			ans+=(ll) (min(r1,lower(x,1,n,ql+1)-len)-(ql-len+1)+1) * (lower(x,1,n,ql+1)-ln+1);
		}
		else{
			node tmp=query(x,1,n,ql+1,qr);
			ans+=tmp.sum-(ll) (ln-1) * (tmp.max-tmp.min);
			if(ql==0) ans+=(ll) (l1-2) * (r1-ln+1);
			else ans+=(ll) (min(r1,lower(x,1,n,ql+1)-len)-(ql-len+1)+1) * (lower(x,1,n,ql+1)-ln+1);
			if(qr==rn) ans+=(ll) (n-r1+n-ln) * (r1-ln+1) / 2;
			else ans+=(ll) (min(r1,lower(x,1,n,qr+1)-len)-(qr-len+1)+1) * (lower(x,1,n,qr+1)-ln+1);
		}
		return ans;
	}
}
// Suffix Automaton
namespace SAM{
	int last=1,tot=1;
	int ch[N][10],fa[N],len[N],pos[N]; // pos:out->in
	int root[N]; // for Interval Tree
	void extend(int c,int po){
		int p=last,cur=last=++tot;
		len[cur]=len[p]+1,pos[po]=cur;
		T::insert(root[cur],1,n,po);
		for(;p&&!ch[p][c];p=fa[p]) ch[p][c]=cur;
		if(!p) fa[cur]=1;
		else{
			int q=ch[p][c];
			if(len[q]==len[p]+1) fa[cur]=q;
			else{
				int clone=++tot;
				memcpy(ch[clone],ch[q],sizeof ch[q]);
				fa[clone]=fa[q],len[clone]=len[p]+1;
				fa[cur]=fa[q]=clone;
				for(;ch[p][c]==q;p=fa[p]) ch[p][c]=clone;
			}
		}
	}
	int anc[N][19];
	vector<int> e[N];
	vector<pair<int,int> > q[N];
	void init(){
		for(int i=1;i<=n;++i) extend(s[i]-'0',i);
		for(int i=1;i<=tot;++i) anc[i][0]=fa[i],e[fa[i]].push_back(i);
		for(int k=1;k<=18;++k)
			for(int i=1;i<=tot;++i) anc[i][k]=anc[anc[i][k-1]][k-1];
	}
	void storequery(int l,int r,int id){
		int len=r-l+1,p=pos[r];
		for(int i=18;i>=0;--i)
			if(SAM::len[anc[p][i]]>=len) p=anc[p][i];
		q[p].push_back(make_pair(len,id));
	}
	void work(int p){
		for(int i=0;i<e[p].size();++i)
			work(e[p][i]),root[p]=T::merge(root[p],root[e[p][i]]);
		for(int i=0;i<q[p].size();++i)
			ans[q[p][i].second]=(ll)(n-1)*(n-2)/2-T::query(root[p],q[p][i].first-1); // available len for cutting
	}
}

int main(){
	read(n),read(m),scanf("%s",s+1);
	SAM::init();
	for(int l,r,i=1;i<=m;++i){
		read(l),read(r);
		SAM::storequery(l,r,i);
	}
	SAM::work(1);
	for(int i=1;i<=m;++i) printf("%lld\n",ans[i]);
	return 0;
}

强烈推荐cz_xuyixuan的代码,他竟然把码风维护到这种题上。当初我早就放弃封装了。

posted on 2019-05-04 18:15  autoint  阅读(266)  评论(0编辑  收藏  举报

导航