CF997E - Good Subsegments 题解

歪解

一个显然的 observation 是区间 \(l,r\) 合法当且仅当 \(mx_r-mx_l=r-l\)。一开始想这题的时候感觉这东西根本不可维护好吧……那就开始想一些奇怪的数据结构题的算法(比如离线分治或者根号算法或者数据分治),然后想到了分块。就愉快地开始用分块做啦(1.2e5 7s 感觉很可行)!

设每块大小 \(S\)。那么分成左边零散的、右边零散的、中间整块三段,分别记为 L、R、M。设 XY(不是染色体啦)表示左端点在 X,右端点在 Y,则总共分成 6 种贡献:LL、LM、LR、MM、MR、RR。然后还有询问中的 \(l,r\) 在同一块的特殊情况。以及 MM 的贡献又分成同块和不同块。

LL 是某一块的后缀,RR 是某一块的前缀,\(l,r\) 在同一块是某一块的子区间,MM 的第一部分是整块。这四者中第三者最强,解决它就解决所有的了。这其实很容易预处理:有 \(n\) 个位置,每个位置往右边最多去 \(S\) 格,所以只有 \(\mathrm O(nS)\) 个值要预处理,需要均摊常数。那对于每块暴力枚举出来所有合法区间(如何 \(\mathrm O(1)\) 判?ST 表维护 rmq 即可),然后计算该块内的子区间内有多少个合法子区间,就是个二维前缀和的事情了。复杂度 \(\mathrm O\!\left(\dfrac nSS^2\right)=\mathrm O(nS)\)

剩下中的 LM、MR 是不完整的块对整块的贡献,MM 的第二部分是完整的块对完整的块的贡献,貌似前者要强一点。考虑到一般分块对不完整块的处理办法都是暴力枚举,我们这里也尝试这样,那么只需要预处理出每个点对每个块的贡献(然后对块的那一维求前缀和,这 extremely easy)即可,这只有 \(\mathrm O\!\left(n\dfrac nS\right)=\mathrm O\!\left(\dfrac{n^2}S\right)\) 个值要计算(同样需要均摊常数(或 log?)╮(╯▽╰)╭)。如果这个能解决,那么对于 MM 的第二部分,只需要根据预处理出来的东西再预处理块对块的贡献的前缀和即可,这是 easy 的;以及对 LR 可以用类似的方法现场搞。所以接下来的任务就是全心全意想办法预处理这个东西。

考虑枚举两个块,计算左边块的元素对右边整块的贡献(当然反过来也要计算,与前者过程类似,这里以前者举例)。这东西其实很像 cdq 分治的处理的核心部分,从中间切了一刀,只能左边向右边贡献,就很舒适。那么把两块中间一部分的 max / min 搞出来混到两边,两块中的元素的 \(mx_i\) 表示该位置到中间的 max,\(mn_i\) 类似,那么易证两者在两块不同方向的单调性。那么左边的 \(i\) 和右边的 \(j\) 满足条件当且仅当 \(\max(mx_i,mx_j)-\min(mn_i,mn_j)=j-i\)

由于左块要细化到每个元素,所以肯定从中间往左边枚举左块的元素。这时候对上式的处理,可以分成 \(mx_i?mx_j,mn_i?mn_j\) 四类,分别可以列出 \(f(j)=g(i)\) 的式子。那就把四类分别对应的 \(f(j)\) 全部装在桶里。又根据单调性,可以 two-pointers 好吧。但是这一部分常数真的很大很大(你想想,四类,还 vector)(但是可以调参不是么,而且 TL = 7s,方个 hammer class),也很难写很难写,能把人搞崩溃的好吧(虽然我没崩溃)。

复杂度 \(\mathrm O\!\left(nS+\dfrac{n^2}S+qS\right)\)\(qS\) 是因为 LR 仿照上面预处理的方式现场搞),先设个 \(S=\sqrt n\) 再说。然后考虑实现细节。首先上面预处理了两个 \(\mathrm O(n\sqrt n)\) 的数组是吧,就算开了 int instead of long long,也 540MB 左右,超过了 ML = 512MB。但是你发现第一个数组其实只用了 1/2(因为区间有 \(l\leq r\) 的限制对吧),那就映射一下,今后空间不成问题。然后卡常。注意到上面这个常数很大的部分要算两遍(左对右 and vice versa),而现场搞只需要搞一遍,那么 \(S\) 应该稍大一点(此处认为 \(n=q\)(?)),大概是翻个 \(\sqrt 2\) 倍的样子吧。经过不懈的尝试,在 \(S=1.618\sqrt n\) 开 Ofast(O3 都不行 hh)的情况下以 6988ms 的运行时间 A 掉了。

code(你不会想看的,trust me!6k 4h 的代码[多多大笑],《真的很好地锻炼了代码能力》。)
#pragma GCC optimize("Ofast")
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define pb push_back
const int inf=0x3f3f3f3f3f3f3f3f;
const int N=150000,LOG_N=20,SZ=800,SZ0=250;
int n,qu,sz,sz1;
int a[N];
struct stable{
	signed mn[N][LOG_N],mx[N][LOG_N],_log[N];
	void init(){
		for(int i=2;i<=n;i++)_log[i]=_log[i-1]+(i==1<<_log[i-1]+1);
		for(int i=1;i<=n;i++)mn[i][0]=mx[i][0]=a[i];
		for(int j=1;j<LOG_N;j++)for(int i=1;i+(1<<j)-1<=n;i++)
			mn[i][j]=min(mn[i][j-1],mn[i+(1<<j-1)][j-1]),mx[i][j]=max(mx[i][j-1],mx[i+(1<<j-1)][j-1]);
	}
	int _mn(int l,int r){
		if(l>r)return inf;
		int log0=_log[r-l+1];
		return min(mn[l][log0],mn[r-(1<<log0)+1][log0]);
	}
	int _mx(int l,int r){
		if(l>r)return -inf;
		int log0=_log[r-l+1];
		return max(mx[l][log0],mx[r-(1<<log0)+1][log0]);
	}
}st;
signed inside[N][SZ/2];
int Sum[SZ][SZ];
signed cnt[N][SZ0];
int mn[N],mx[N];
vector<signed> xnl[3*N],nl[3*N],xl[3*N];
short xnl0[3*N],nl0[3*N],xl0[3*N];
short xnl1[3*N],nl1[3*N],xl1[3*N];
int eqrg(vector<signed> ll[],short l0[],short l1[],int l,int r,int v){
	while(l0[v]+1<ll[v].size()&&ll[v][l0[v]+1]<l)l0[v]++;
	while(l1[v]+1<ll[v].size()&&ll[v][l1[v]+1]<=r)l1[v]++;
	return max(0,l1[v]-l0[v]);
}
int eqrg0(vector<signed> ll[],short l0[],short l1[],int l,int r,int v){
	while(l0[v]+1<ll[v].size()&&ll[v][l0[v]+1]>l)l0[v]++;
	while(l1[v]+1<ll[v].size()&&ll[v][l1[v]+1]>=r)l1[v]++;
	return max(0,l1[v]-l0[v]);
}
int rgcnt[SZ][SZ];
signed &id(int x,int len){
	int p=(x+sz1-1)/sz1;
	int l=sz1*(p-1)+1;
	x=x-l+1;
	return inside[l][(sz1+sz1-x+2)*(x-1)/2+len];
}
signed main(){
//	cout<<(sizeof(inside)+sizeof(cnt)+sizeof(st)+2*sizeof(Sum)+4*sizeof(xnl)+8*sizeof(xnl0))/1024/1024;
//	freopen("read.in","r",stdin);
	cin>>n;
	for(int i=1;i<=n;i++)scanf("%lld",a+i);
//	cout<<clock()<<"!\n";
	st.init();
	sz1=1.618*sqrt(n),sz=(n+sz1-1)/sz1;
	for(int i=1;i<=sz;i++){
		memset(Sum,0,sizeof(Sum));
		int l=sz1*(i-1)+1,r=min(n,sz1*i);
		for(int j=l;j<=r;j++)for(int k=j;k<=r;k++)if(st._mx(j,k)-st._mn(j,k)==k-j)Sum[j-l+1][k-l+1]=1;
		for(int j=1;j<=r-l+1;j++)for(int k=1;k<=r-l+1;k++)Sum[j][k]+=Sum[j-1][k]+Sum[j][k-1]-Sum[j-1][k-1];
		for(int j=l;j<=r;j++)for(int k=j;k<=r;k++)id(j,k-j+1)=Sum[r-l+1][k-l+1]-Sum[j-l][k-l+1];
	}
//	for(int i=1;i<=n;i++)for(int j=1;j<=sz1;j++)printf("inside[%lld][%lld]=%lld\n",i,j,inside[i][j]);
	#define mem(x) memset(x,-1,sizeof(x))
	mem(xnl0),mem(nl0),mem(xl0),mem(xnl1),mem(nl1),mem(xl1);
	for(int i=1;i<=sz;i++)for(int j=i+1;j<=sz;j++){//remember to make it first
		int l1=sz1*(i-1)+1,r1=min(n,sz1*i),l2=sz1*(j-1)+1,r2=min(n,sz1*j);
		for(int k=l1;k<=r1;k++)mn[k]=st._mn(k,l2-1),mx[k]=st._mx(k,l2-1);
		for(int k=l2;k<=r2;k++)mn[k]=st._mn(r1+1,k),mx[k]=st._mx(r1+1,k);
		for(int k=l2;k<=r2;k++)xnl[mx[k]-mn[k]-k+2*N].pb(k),nl[-mn[k]-k+2*N].pb(k),xl[mx[k]-k+2*N].pb(k);
		int now1=l2-1,now2=l2-1;
		for(int k=r1;k>=l1;k--){
			while(now1<r2&&mn[now1+1]>=mn[k])now1++;while(now2<r2&&mx[now2+1]<=mx[k])now2++;
			cnt[k][j]+=eqrg(xnl,xnl0,xnl1,max(now1+1,now2+1),inf,-k+2*N);
			cnt[k][j]+=eqrg(nl,nl0,nl1,now1+1,now2,-k-mx[k]+2*N);
			cnt[k][j]+=eqrg(xl,xl0,xl1,now2+1,now1,-k+mn[k]+2*N);
			cnt[k][j]+=l2<=k+mx[k]-mn[k]&&k+mx[k]-mn[k]<=min(min(now1,now2),r2);
		}
		for(int k=l2;k<=r2;k++)xnl[mx[k]-mn[k]-k+2*N].clear(),nl[-mn[k]-k+2*N].clear(),xl[mx[k]-k+2*N].clear(),xnl0[mx[k]-mn[k]-k+2*N]=nl0[-mn[k]-k+2*N]=xl0[mx[k]-k+2*N]=xnl1[mx[k]-mn[k]-k+2*N]=nl1[-mn[k]-k+2*N]=xl1[mx[k]-k+2*N]=-1;
	}
	for(int i=1;i<=sz;i++)for(int j=i+1;j<=sz;j++){//remember to make it first
		int l1=sz1*(i-1)+1,r1=min(n,sz1*i),l2=sz1*(j-1)+1,r2=min(n,sz1*j);
		for(int k=l1;k<=r1;k++)mn[k]=st._mn(k,l2-1),mx[k]=st._mx(k,l2-1);
		for(int k=l2;k<=r2;k++)mn[k]=st._mn(r1+1,k),mx[k]=st._mx(r1+1,k);
		for(int k=r1;k>=l1;k--)xnl[mx[k]-mn[k]+k+N].pb(k),nl[-mn[k]+k+N].pb(k),xl[mx[k]+k+N].pb(k);
		int now1=r1+1,now2=r1+1;
		for(int k=l2;k<=r2;k++){
			while(now1>l1&&mn[now1-1]>=mn[k])now1--;while(now2>l1&&mx[now2-1]<=mx[k])now2--;
			cnt[k][i]+=eqrg0(xnl,xnl0,xnl1,min(now1-1,now2-1),0,k+N);
			cnt[k][i]+=eqrg0(nl,nl0,nl1,now1-1,now2,k-mx[k]+N);
			cnt[k][i]+=eqrg0(xl,xl0,xl1,now2-1,now1,k+mn[k]+N);
			cnt[k][i]+=max(l1,max(now1,now2))<=k-mx[k]+mn[k]&&k-mx[k]+mn[k]<=r1;
		}
		for(int k=l1;k<=r1;k++)xnl[mx[k]-mn[k]+k+N].clear(),nl[-mn[k]+k+N].clear(),xl[mx[k]+k+N].clear(),xnl0[mx[k]-mn[k]+k+N]=nl0[-mn[k]+k+N]=xl0[mx[k]+k+N]=xnl1[mx[k]-mn[k]+k+N]=nl1[-mn[k]+k+N]=xl1[mx[k]+k+N]=-1;
	}
	for(int i=1;i<=n;i++)for(int j=1;j<=sz+1;j++)cnt[i][j]+=cnt[i][j-1];
//	cout<<cnt[1][3]<<"!\n";
	for(int i=1;i<=sz;i++)for(int j=1;j<=sz+1;j++){
		int l1=sz1*(i-1)+1,r1=min(n,sz1*i);
		for(int k=l1;k<=r1;k++)rgcnt[i][j]+=cnt[k][j];
	}
	cin>>qu;
	while(qu--){
		int l,r;
		scanf("%lld%lld",&l,&r);
		int pl=(l+sz1-1)/sz1,pr=(r+sz1-1)/sz1;
		if(pl==pr){printf("%d\n",id(l,r-l+1));continue;}
		int res=id(l,sz1*pl-l+1)+id(sz1*(pr-1)+1,r-sz1*(pr-1));
		for(int i=pl+1;i<pr;i++)res+=id(sz1*(i-1)+1,sz1);
		for(int i=pl+1;i<pr;i++)res+=rgcnt[i][pr-1]-rgcnt[i][i];
		for(int i=l;i<=sz1*pl;i++)res+=cnt[i][pr-1]-cnt[i][pl];
		for(int i=sz1*(pr-1)+1;i<=r;i++)res+=cnt[i][pr-1]-cnt[i][pl];
		
		int i=pl,j=pr,l1=l,r1=min(n,sz1*i),l2=sz1*(j-1)+1,r2=r;
		for(int k=l1;k<=r1;k++)mn[k]=st._mn(k,l2-1),mx[k]=st._mx(k,l2-1);
		for(int k=l2;k<=r2;k++)mn[k]=st._mn(r1+1,k),mx[k]=st._mx(r1+1,k);
		for(int k=l2;k<=r2;k++)xnl[mx[k]-mn[k]-k+2*N].pb(k),nl[-mn[k]-k+2*N].pb(k),xl[mx[k]-k+2*N].pb(k);
		int now1=l2-1,now2=l2-1;
		for(int k=r1;k>=l1;k--){
			while(now1<r2&&mn[now1+1]>=mn[k])now1++;while(now2<r2&&mx[now2+1]<=mx[k])now2++;
			res+=eqrg(xnl,xnl0,xnl1,max(now1+1,now2+1),inf,-k+2*N);
			res+=eqrg(nl,nl0,nl1,now1+1,now2,-k-mx[k]+2*N);
			res+=eqrg(xl,xl0,xl1,now2+1,now1,-k+mn[k]+2*N);
			res+=l2<=k+mx[k]-mn[k]&&k+mx[k]-mn[k]<=min(min(now1,now2),r2);
		}
		for(int k=l2;k<=r2;k++)xnl[mx[k]-mn[k]-k+2*N].clear(),nl[-mn[k]-k+2*N].clear(),xl[mx[k]-k+2*N].clear(),xnl0[mx[k]-mn[k]-k+2*N]=nl0[-mn[k]-k+2*N]=xl0[mx[k]-k+2*N]=xnl1[mx[k]-mn[k]-k+2*N]=nl1[-mn[k]-k+2*N]=xl1[mx[k]-k+2*N]=-1;
	
		printf("%lld\n",res);
	}
	return 0;
}

正解

学了一下正解啦。上面我的歪解是在线的。考虑离线下来,那对这个区间查询的问题就很自然的想到扫描线啦——右端点递增,实时维护左端点们处的答案。

首先一路上,\(mx_{l,r},mn_{l,r}\) 可以分别用单调栈维护相等段。注意一个事实:直接用单调栈维护二元组 \((mx_{l,r},mn_{l,r})\) 的相等段复杂度是错的!这是做不到的。

反正可以做到对 \((mx_{l,r}-mn_{l,r})-(r-l)\) 这个式子的值关于 \(l\) 实时维护啦:前两者单调栈 + 区间增加,第三者区间 -1,第四者不动对吧。先考虑只求右端点为 \(r\),左端点为 \(l\sim r\) 的合法区间个数,就归约到一个经典问题:区间增加、区间查询 \(0\) 的个数。如果没有特殊条件,这个大概只能分块做了吧。但是有个很 immortal 的 observation:\((mx_{l,r}-mn_{l,r})-(r-l)\) 这个式子永远大于等于 \(0\)。证明显然,但是很难观察到。那有了这个 observation,\(0\) 由平平无奇的值变成了可能的最小值,那只需要按照 little elephant 那题的 rng 的套路,线段树维护区间最小值以及最小值出现次数即可。

但现在要求的是 \([l,r]\) 内合法子区间的个数啊。如果想让之前的思考派上用场,可以发现这其实就是有史以来(即右端点从 \(l\)\(r\) 递增的这段时间里),\([l,r]\) 区间中每次查询得到的答案的总和。哟吼,维护历史问题我可来劲了,要知道这也是线段树懒标记擅长维护的东西。我们考虑维护线段树上每个节点区间内的出现过的历史最小值,以及该历史最小值出现过的历史总次数

你用线段树维护这东西得满足:可合并,区间修改的时候分到底可以快速更新,能够设计出可压缩的懒标记并且可以快速下传。维护当前最小值,当前最小值出现次数,出现过的历史最小值,以及该历史最小值出现过的历史总次数,第一者和第二者实在是很容易好吧。考虑第三者,思考一般情况:如果一个点处,多次修改都以它为分到底的区间之一,留下了多个懒标记痕迹;有一天我要下传它的懒标记了,我该如何设计可压缩可下传的懒标记?

考虑一般情况下,任意时刻需要下传的是一系列区间加的操作(但是操作序列太大了),显然只有加的值是重要的,考虑这个加的值的序列。注意到任意时刻,线段树内的任意处的最小值永远 \(\geq 0\) 的这个 observation,那么我这个序列的某一处前缀和如果不是最小,那此时必定整个区间没有 \(0\),那就没有用了。也就是我们只需要关心那些前缀和最小的地方。于是我们需要记录,从上一次懒标记被下传到现在的懒标记序列的前缀和中最小的是多少,以及这个最小值出现了多少遍。懒标记设计出来后,一切都 easy 了。

复杂度线对。

code(比较容易写吧,主要是难想)
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define mp make_pair
#define X first
#define Y second
const int inf=0x3f3f3f3f3f3f3f3f;
const int N=150000;
int n,qu;
int a[N];
pair<pair<int,int>,int> qry[N];
bool cmp(pair<pair<int,int>,int> x,pair<pair<int,int>,int> y){return x.X.Y<y.X.Y;}
int ans[N];
struct segtree{
	struct node{int mn,tm,hmn,htm,lz,hlz,hlztm;}nd[N<<2];
	#define mn(p) nd[p].mn
	#define tm(p) nd[p].tm
	#define hmn(p) nd[p].hmn
	#define htm(p) nd[p].htm
	#define lz(p) nd[p].lz
	#define hlz(p) nd[p].hlz
	#define hlztm(p) nd[p].hlztm
	void bld(int l=1,int r=n,int p=1){
		mn(p)=hmn(p)=inf,tm(p)=htm(p)=r-l+1,lz(p)=hlz(p)=hlztm(p)=0;
		if(l==r)return;
		int mid=l+r>>1;
		bld(l,mid,p<<1),bld(mid+1,r,p<<1|1);
	}
	void init(){bld();}
	void sprup(int p){
		if(mn(p<<1)<mn(p<<1|1))mn(p)=mn(p<<1),tm(p)=tm(p<<1);
		else if(mn(p<<1)>mn(p<<1|1))mn(p)=mn(p<<1|1),tm(p)=tm(p<<1|1);
		else mn(p)=mn(p<<1),tm(p)=tm(p<<1)+tm(p<<1|1);
		if(hmn(p<<1)<hmn(p<<1|1))hmn(p)=hmn(p<<1),htm(p)=htm(p<<1);
		else if(hmn(p<<1)>hmn(p<<1|1))hmn(p)=hmn(p<<1|1),htm(p)=htm(p<<1|1);
		else hmn(p)=hmn(p<<1),htm(p)=htm(p<<1)+htm(p<<1|1);
	}
	void tag(int p,int v,int hv,int hvtm){
		if(mn(p)+hv<hmn(p))hmn(p)=mn(p)+hv,htm(p)=tm(p)*hvtm;
		else if(mn(p)+hv==hmn(p))htm(p)+=tm(p)*hvtm;
		mn(p)+=v;
		if(lz(p)+hv<hlz(p))hlz(p)=lz(p)+hv,hlztm(p)=hvtm;
		else if(lz(p)+hv==hlz(p))hlztm(p)+=hvtm;
		lz(p)+=v;
	}
	void sprdwn(int p){
		tag(p<<1,lz(p),hlz(p),hlztm(p)),tag(p<<1|1,lz(p),hlz(p),hlztm(p));
		lz(p)=hlz(p)=hlztm(p)=0;
	}
	void add(int l,int r,int v,int p=1,int tl=1,int tr=n){
		if(l<=tl&&r>=tr)return tag(p,v,v,1);
		sprdwn(p);
		int mid=tl+tr>>1;
		if(l<=mid)add(l,r,v,p<<1,tl,mid);
		if(r>mid)add(l,r,v,p<<1|1,mid+1,tr);
		sprup(p);
	}
	int h0(int l,int r,int p=1,int tl=1,int tr=n){
		if(l<=tl&&r>=tr)return hmn(p)==0?htm(p):0;
		sprdwn(p);
		int mid=tl+tr>>1,res=0;
		if(l<=mid)res+=h0(l,r,p<<1,tl,mid);
		if(r>mid)res+=h0(l,r,p<<1|1,mid+1,tr);
		return res;
	}
}segt;
stack<pair<int,int> > mn,mx;
signed main(){
	cin>>n;
	for(int i=1;i<=n;i++)scanf("%lld",a+i);
	cin>>qu;
	for(int i=1;i<=qu;i++)scanf("%lld%lld",&qry[i].X.X,&qry[i].X.Y),qry[i].Y=i;
	sort(qry+1,qry+qu+1,cmp);
	segt.init();
//	puts("ok");
	mn.push(mp(0,0)),mx.push(mp(0,n+1));
	int now=0;
	for(int i=1;i<=n;i++){
		while(a[i]<mn.top().Y){
			int r=mn.top().X,v=mn.top().Y;mn.pop();int l=mn.top().X+1;
			segt.add(l,r,abs(v-a[i]));
		}
		while(a[i]>mx.top().Y){
			int r=mx.top().X,v=mx.top().Y;mx.pop();int l=mx.top().X+1;
			segt.add(l,r,abs(v-a[i]));
		}
		segt.add(i,i,-inf+1);
		mn.push(mp(i,a[i])),mx.push(mp(i,a[i]));
		segt.add(1,i,-1);
		while(now<qu&&qry[now+1].X.Y==i){
			now++;
			ans[qry[now].Y]=segt.h0(qry[now].X.X,qry[now].X.Y);
		}
	}
	for(int i=1;i<=qu;i++)printf("%lld\n",ans[i]);
	return 0;
}
posted @ 2021-07-13 00:16  ycx060617  阅读(118)  评论(0编辑  收藏  举报