洛谷P5386 题解-今日卡常学习

洛谷P5386

题目大意:

给定一个长度为 \(n\) 排列 \(p\),给定 \(q\) 次询问,每次给定一个四元组 $(l,r,x,y) $,问满足如下条件的二元组 $(u,v) $ 的数量:

  • $l \le u \le v\le r $;
  • 对于 \(\forall u\le i \le v\),满足 \(x \le p_i\le y\)

$ 1\le n,q \le 2\times 10^5$

做法分析:

注意到这是一道数据结构题,并且有四个自由度。不妨考虑先莫队干掉两个自由度。

做题经验得知干掉 \(x,y\) 比较好。

大致思考如下:如果维护 \(l,r\),值域会突变。我们本质是要找极长的连续段 \((u,v)\) 满足 \(\forall u\le i \le v\),满足 \(x \le p_i\le y\)(并对其求平方)。朴素思维是维护所有大于 \(y\) 和小于 \(x\) 的断点,随后便不难计算。但是由于值域突变、没有良好性质和莫队外 \((x,y)\) 自身的变化使我们难以维护这个连续段的值。因此考虑按值域莫队。

朴素思考就是根据值域把每个能够插入的 \(p\) 的下标计入,由于值域只有 \(O(n)\),不难想到直接在值域上开个数组。然后,就变成了维护一个 \(01\) 串,要求会单点修改,区间查询所有级长连续 \(1\) 段的长度的平方。

显然线段树可做。时间复杂度 $O(n \sqrt{n} \log n) $。正常来讲卡不过去。接下来就需要一些奇妙的优化……了吗?

但我并不正常!

注意到,这个东西实际的“瓶颈”在于莫队的 \(O(n \sqrt{n})\) 次修改,但是询问只有 \(O(q)\) 次。不难想到,我们或许不能再复杂度上做文章,但是我们可以把修改的常数均摊到询问上,使得我们的常数十分优秀,以至于能推平一个 \(\log\) 的差距!

考虑如何均摊。莫队是动不了了,不妨考虑对萌萌哒线段树下手。不难发现修改的复杂度严格与树高有关,不妨考虑给可怜的线段树砍几层。cos路易十六

考虑一颗线段树,思考哪些节点是没用的。首先,根节点没用,谁闲没事干查整个区间啊不会真有吧;类似的,其下两个儿子也没啥用,谁闲没事干查包含一个端点且直接延伸到另一半那么长那么特殊的区间啊;类似的,其下的四个儿子……

不难发现,实际上线段树越“高”(深度上是越低)的位置相对更没那么有用。就算有用也可以被他的两个儿子代替。因此,不难想到一个很魔怔的做法——开一个块长为 \(2^k\) 的块,然后每个块内跑线段树。这样的话修改的复杂度就只有 \(O(k)\) 了。至于询问,管他呢,反正不是瓶颈其实简单分析一下就知道是询问次数 \(O(q)\),单次询问是整块直接调用根节点信息 \(O(1)\) 乘块的数量 \(O(\frac{n}{2^k})\),散块就是区间查找 \(O(k)\),显然合并 \(O(1)\),和在一块即 \(O(q (k+\frac{n}{2^k}))\),在加上莫队就是 $ O(nk \sqrt{n} +q (k+\frac{n}{2^k})) $。

反正能过

代码

AC

//I often reminisce about the past
#include<bits/stdc++.h>
using namespace std;
bool Mst;
#define LL long long
#define For(a,b,c) for(int (a)=(b);(a)<=(c);(a)++)
#define Rep(a,b,c) for(int (a)=(b);(a)>=(c);(a)--)
const int N=2e5+10,M=3e5+10,K=5e3+3,Mod=998244353;
//const int INF=0x3f3f3f3f;
const LL INF=0x3f3f3f3f3f3f3f3f,all=1ll;
int n,m,k;

template <typename T> void read(T &a){
	char c=getchar();T w=1,f=0;while(c<'0'||c>'9'){if(c=='-') w=-w;c=getchar();}
	while(c>='0'&&c<='9') f=f*10+c-'0',c=getchar();a=f*w;
}
template <typename T> void tomin(T &a,const T &b){if(b<a) a=b;}
template <typename T> void tomax(T &a,const T &b){if(a<b) a=b;}
void modadd(int &a,const int &b,const int &mod=Mod){a+=b;a<0?a+=Mod:a>=Mod?a-=Mod:0;}
void modsub(int &a,const int &b,const int &mod=Mod){a-=b;a<0?a+=Mod:a>=Mod?a-=Mod:0;}
void modmul(int &a,const int &b,const int &mod=Mod){a=1ll*a*b%mod;}
int ksm(int a,int b,int mod=Mod){int res=1;while(b){if(b&1) modmul(res,a,mod);modmul(a,a,mod);b>>=1;}return res;}
int __inv(int x){return ksm(x,Mod-2,Mod);}

int a[N];
struct Q{
	int l,r,x,y,id,ans;
}q[N];

//根据下标分块,即线段树部分
const int B=128;
int B_rg=545;

struct A{
	bool flag;
	int nl,nr;
	LL num;
	//这里使用 operator好像常数会小一点,至少本地测试会
	friend A operator + (const A &L,const A &R)
	{
		A res;
		res.num=L.num+R.num+1ll*L.nr*R.nl;
		res.nl=L.nl+L.flag*R.nl;
		res.nr=R.nr+R.flag*L.nr;
		res.flag=L.flag&R.flag;
		return res;
	}
};

struct Tree{
	int l,r,lc,rc;//这里把数据直接存起来好像比现算更快
	A x;
};
int at[B+3];
struct Block{
	int l,r;
	Tree t[(B+1)<<2];
	inline void build(int root,int l,int r)
	{
		t[root].l=l,t[root].r=r;
		if(l==r)
		{
			at[l]=root;
			return ;
		}
		int &lc=t[root].lc,&rc=t[root].rc;
		lc=root<<1;rc=lc|1;
		int mid=(l+r)>>1;
		build(lc,l,mid);build(rc,mid+1,r);
	}
	inline void update(int r,const int &k)//这里是瓶颈
	{
		r=at[r];
		t[r].x.num=t[r].x.nl=t[r].x.nr=t[r].x.flag=k;
		while(r>>=1)//这里把递归改成循环能显著降低常数。众所周知,递归常数远大于循环。
		{
			A &L=t[t[r].lc].x,&R=t[t[r].rc].x;
			t[r].x.num=L.num+R.num+1ll*L.nr*R.nl;
			t[r].x.nl=L.nl+L.flag*R.nl;
			t[r].x.nr=R.nr+R.flag*L.nr;
			t[r].x.flag=L.flag&R.flag;
		}
	}
	inline A query(int root,int l,int r)
	{
		if(t[root].l>=l&&r>=t[root].r) return t[root].x;
		if(l<=t[t[root].lc].r&&r>=t[t[root].rc].l) return query(t[root].lc,l,r)+query(t[root].rc,l,r);
		else if(l<=t[t[root].lc].r) return query(t[root].lc,l,r);
		else return query(t[root].rc,l,r);
	}
}b[N/B+2];
int bs[N];

inline void move(int id,int f)
{
	int x=a[id];
	b[bs[x]].update(x-b[bs[x]].l+1,f);
}
inline LL query(int L,int R)
{
	if(bs[L]==bs[R])
	{
		return b[bs[L]].query(1,L-b[bs[L]].l+1,R-b[bs[L]].l+1).num;
	}
	A res=b[bs[L]].query(1,L-b[bs[L]].l+1,b[bs[L]].r-b[bs[L]].l+1);
	For(i,bs[L]+1,bs[R]-1)
	{
		res=res+b[i].t[1].x;
	}
	res=res+b[bs[R]].query(1,1,R-b[bs[R]].l+1);
	return res.num;
}

//值域分块
int bs_rg[N];

int p[N];
int ans[N];

int Test=1;
void mian()
{
	read(n);read(m);
	For(i,1,n) read(p[i]);
	For(i,1,n) a[p[i]]=i;
	For(i,1,m) read(q[i].l),read(q[i].r),read(q[i].x),read(q[i].y),q[i].id=i;
	
	For(i,1,n)
	{
		bs[i]=(i-1)/B+1;
		if(bs[i]!=bs[i-1]) b[bs[i]].l=i;
		b[bs[i]].r=i; 
	}
	For(i,1,n) bs_rg[i]=(i-1)/B_rg+1;
	sort(q+1,q+1+m,[](const Q &q1,const Q &q2){
		const int &b1=bs_rg[q1.x],&b2=bs_rg[q2.x];
		return b1!=b2?b1<b2:b1&1?q1.y<q2.y:q1.y>q2.y;//奇偶排序显著降低常数
	});
	For(i,1,bs[n]) b[i].build(1,1,B);
	
	int L=1,R=0;
	For(i,1,m)
	{
		fflush(stdout);
		int l=q[i].x,r=q[i].y;
		while(R<r){//把move函数下放显著降低常数,众所周知函数调用的常数极大。
			int x=a[++R];
			b[bs[x]].update(x-b[bs[x]].l+1,1);
		}
		while(L>l){
			int x=a[--L];
			b[bs[x]].update(x-b[bs[x]].l+1,1);
		}
		while(L<l){
			int x=a[L++];
			b[bs[x]].update(x-b[bs[x]].l+1,0);
		}
		while(R>r)
		{
			int x=a[R--];
			b[bs[x]].update(x-b[bs[x]].l+1,0);
		}
		ans[q[i].id]=query(q[i].l,q[i].r);
	}
	
	For(i,1,m)
	{
		printf("%d\n",ans[i]);
	}
}
bool Med;
signed main()
{
	//cerr<<(&Mst-&Med)/1024.0/1024.0<<" MB\n";//其实cerr就算无效也会占用常数,但影响不大。
	// freopen("debug.in","r",stdin);
	// freopen("debug.out","w",stdout);
	//cin>>Test;
	while(Test--) mian();
	return 0;
}
posted @ 2026-01-18 11:51  FarrisL  阅读(1)  评论(0)    收藏  举报