(简记)扫描线 离线二维数点

本来想丢到其他 blog 里面的,后来发现还是太臃肿了。

扫描线

一种二维问题的解决方式。我们常说对 \(x\) 作扫描线,通常是指存在这样一类二维的偏序关系,我们用一个指针扫一维,然后用数据结构处理第二维。这类关系使得对于操作 \((x,y)\) 和询问 \((l,r,l',r')\),有不等式 \(l'\le x \le r'\),且要求 \(l\le y \le r\)(有时这些操作点 \((x,y)\) 还会进一步进化成操作矩形,就像矩形面积并),这时候我们称该操作对询问有贡献。扫描线就是帮助你将操作和询问贡献快速计算出来的一种离线技巧。如何对经典问题是二维平面矩形周长并和面积并,用一个循环跑 \(x\) 坐标,\(y\) 坐标丢进线段树里处理。假设每个矩形左下角 \((x_a,y_a)\),右上角 \((x_b,y_b)\),那么需要在 \(x_a\) 加入一个 \([y_a,y_b]\) 的线段树区间加,然后在 \(x_b+1\) 减去一个 \([y_a,y_b]\)。一般情况下这种扫描线还需要用到离散化。

扫描线有几种常用实现方式(个人来说):

  1. 离散化扫描轴,或扫描轴本身较小时,可以直接跑扫描轴,用一个 vector<pair<int,int> > 之类的东西记录修改操作。

  2. 排序操作和询问,然后类似双指针地解决问题,外面是询问循环,里面推操作循环。

P5490 【模板】扫描线 & 矩形面积并

实际上这里的线段树应用是一个很容易混淆的问题。我们首先要搞清楚线段树怎么维护这些东西。

  • 经典例题是 \([l,r]\) 的区间加区间求和。在这里我们相当于给了每个端点下标)一个点权,然后每次查询区间内端点编号的点权和,我们称之为点信息

  • 在数轴上维护信息时,我们不能这么认为,因为要修改的信息已不再是固定在点上的,我们真正要修改一条线段对应的区间,如矩形面积并,也就是要维护边信息。但是我们的线段树下标仍然是对应着点信息的。于是我们要用一种方法把边信息扭成点信息

    解决方法 1:通过数轴端点(很多,一般离散化出有用的)划分区间,如 \(0\) 对应 \([0,1]\) 的覆盖信息, \(1\) 对应 \([1,2]\) 的覆盖信息等等,即让 \([l,r]\) 区间的端点代表标号为 \([l,r]\) 的线段区间(实际涵盖 \([l,r+1]\) 的端点)。

    解决方法 2:分治时划分区间 \([l,r]\)\([l,mid],[mid,r]\),强制令最小区间为 \([a,a+1]\),这样也是正确的。代码中演示的就是这种方法,不知道为什么常数莫名大,但是空间占用小了一般(丢掉了最底层的叶子节点吧)。

有时候题目需要我们维护一些很诡异的信息,维护区间内大于 \(0\) 的数的个数(保证所有数时刻 \(\geq 0\)),且保证所有区间加都对应一个唯一的撤销操作(扫描线的拆分方式)。对于区间修改,常规方法是分裂成 \(O(\log n)\) 个线段树上节点,对于每个节点维护一个 \(\texttt{res}\)\(\texttt{cov}\)\(\texttt{cov}\) 表示当前节点被区间直接覆盖的次数,\(\texttt{res}\) 表示区间中大于 0 的数个数,区间加不需要下放 \(\texttt{tag}\),因为不便于撤销。查询也是直接返回 \(\texttt{res}\) 即可。这样每次 \(\texttt{cov}\) 必定会被加某个值然后在某个时刻减回来,全局查询正确性显然,如果是区间查询就需要在往下递归拆分询问区间时,如果经过的节点 \(\texttt{cov}\) 不为 0,那么直接返回都被覆盖的情况。这种维护颇具局限性,几乎只有这种应用场景,线段树上维护偏序信息和区间加减本身就是一件挺困难的事,这只是一种特殊应用。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=2e5+5;
int lsh[N],cnt,n,pos,cpt;
struct Sgt{
	#define ls p<<1
	#define rs p<<1|1
	#define mid ((l+r)>>1)
	int cov[N<<2],res[N<<2];
	void pushup(int p,int l,int r){
		if(cov[p])res[p]=lsh[r]-lsh[l];
		else res[p]=res[ls]+res[rs];
	}
	void update(int p,int l,int r,int L,int R,int v){
		if(l==r)return ;
		if(L<=l&&r<=R){
			cov[p]+=v;
			if(l+1<r)pushup(p,l,r);
			else {
				if(cov[p])res[p]=lsh[r]-lsh[l];
				else res[p]=0;
			}
			return ;
		}
		if(L<mid)update(ls,l,mid,L,R,v);
		if(R>mid)update(rs,mid,r,L,R,v);
		pushup(p,l,r);
	}
	#undef ls
	#undef rs
	#undef mid
}T;
struct Q{int pos,l,r,op;}q[N];
bool cmp(Q x,Q y){return x.pos<y.pos;}
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++){
		int xa,ya,xb,yb;
		scanf("%d%d%d%d",&xa,&ya,&xb,&yb);
		if(xa>xb)swap(xa,xb);
		if(ya>yb)swap(ya,yb);
		lsh[++cnt]=ya,lsh[++cnt]=yb;
		q[++cpt]=(Q){xa,ya,yb,1};
		q[++cpt]=(Q){xb,ya,yb,-1};
	}
	sort(lsh+1,lsh+1+cnt);
	cnt=unique(lsh+1,lsh+1+cnt)-(lsh+1);
	sort(q+1,q+1+cpt,cmp);
	long long ans=0;
	for(int i=1;i<cpt;i++){
		q[i].l=lower_bound(lsh+1,lsh+1+cnt,q[i].l)-lsh;
		q[i].r=lower_bound(lsh+1,lsh+1+cnt,q[i].r)-lsh;
		T.update(1,1,cnt,q[i].l,q[i].r,q[i].op);
		if(q[i].pos!=q[i+1].pos)ans+=1ll*T.res[1]*(q[i+1].pos-q[i].pos);
	}
	printf("%lld",ans);
	return 0;
}

P1856 [IOI 1998 / USACO5.5] 矩形周长 Picture(矩形周长并)

写了我 \(10^{10^{100}}\) 年,如此水平,何以 OI???

逻辑是 \(x\) 轴和 \(y\) 轴都分别扫一遍,然后每次先加边后减边,记录每次变化的线段 \([a,a+1]\) 的数量,其和就是答案。为什么要这么做?细化一下周长并模型,以扫描 \(x\) 轴为例。我们在 \(x_a\) 处加入 \([y_a,y_b]\),实际上是在 \(x=x_a\) 处加入了一条线段,\(x_b\) 处删除 \([y_a,y_b]\) 是在 \(x=x_b\) 处删除一条线段,如果存在一个 \(x\) 既有删除又有增加,那么经过当前 \(x\) 所有操作后 \(>0\)\([a,a+1]\) 就不应该计入答案。我们的思路是先增加,然后记录从 \(0\) 增到 \(>0\)\([a,a+1]\),然后再减,这时候增加覆盖过的 \([a,a+1]\) 都不可能再被减成 \(0\),记录从 \(>0\) 减到 \(0\)\([a,a+1]\) 数量。

时间复杂度 \(O(n\log n)\)

点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
typedef pair<int,int> PII;
const int N=1e4+5;
int n,X,Y;
int cnty,lshy[N],cntx,lshx[N];
struct Sqare{int xa,ya,xb,yb;}S[N];
bool tf;
struct Sgt{
	#define ls p<<1
	#define rs p<<1|1
	#define mid ((l+r)>>1)
	int cov[N<<2],res[N<<2];
	void pushup(int p,int l,int r){
		if(cov[p]){
			if(!tf)res[p]=lshx[r]-lshx[l];
			else res[p]=lshy[r]-lshy[l];
		}
		else res[p]=res[ls]+res[rs];
	}
	void update(int p,int l,int r,int L,int R,int v){
		if(l==r||L>=R)return ;
		if(L<=l&&r<=R){
			cov[p]+=v;
			if(l+1<r)pushup(p,l,r);
			else {
				if(cov[p]){
					if(!tf)res[p]=lshx[r]-lshx[l];
					else res[p]=lshy[r]-lshy[l];
				}
				else res[p]=0;
			}
			return ;
		}
		if(L<mid)update(ls,l,mid,L,R,v);
		if(R>mid)update(rs,mid,r,L,R,v);
		pushup(p,l,r);
	}
	#undef ls
	#undef rs
	#undef mid
}T;
vector<PII>Ins[N],Del[N];
LL ans;
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	cin>>n;
	for(int i=1;i<=n;i++){
		int xa,xb,ya,yb;
		cin>>xa>>ya>>xb>>yb;
		S[i]=(Sqare){xa,ya,xb,yb};
		lshx[++cntx]=xa;
		lshx[++cntx]=xb;
		lshy[++cnty]=ya;
		lshy[++cnty]=yb;
	}
	sort(lshx+1,lshx+1+cntx);
	cntx=unique(lshx+1,lshx+1+cntx)-(lshx+1);
	sort(lshy+1,lshy+1+cnty);
	cnty=unique(lshy+1,lshy+1+cnty)-(lshy+1);
	for(int i=1;i<=n;i++){
		S[i].xa=lower_bound(lshx+1,lshx+1+cntx,S[i].xa)-lshx;
		S[i].xb=lower_bound(lshx+1,lshx+1+cntx,S[i].xb)-lshx;
		S[i].ya=lower_bound(lshy+1,lshy+1+cnty,S[i].ya)-lshy;
		S[i].yb=lower_bound(lshy+1,lshy+1+cnty,S[i].yb)-lshy;
		Ins[S[i].xa].emplace_back(make_pair(S[i].ya,S[i].yb));
		Del[S[i].xb].emplace_back(make_pair(S[i].ya,S[i].yb));
	}
	tf=1;
	for(int i=1;i<=cntx;i++){
		int ori=T.res[1];
		for(PII j:Ins[i])
			T.update(1,1,cnty,j.first,j.second,1);
		ans+=T.res[1]-ori;
		ori=T.res[1];
		for(PII j:Del[i])
			T.update(1,1,cnty,j.first,j.second,-1);
		ans+=ori-T.res[1];
	}
	for(int i=1;i<=cnty;i++)
		Ins[i].clear(),Del[i].clear();
	for(int i=1;i<=n;i++){
		Ins[S[i].ya].emplace_back(make_pair(S[i].xa,S[i].xb));
		Del[S[i].yb].emplace_back(make_pair(S[i].xa,S[i].xb));
	}
	tf=0;
	for(int i=1;i<=cnty;i++){
		int ori=T.res[1];
		for(PII j:Ins[i])
			T.update(1,1,cntx,j.first,j.second,1);
		ans+=T.res[1]-ori;
		ori=T.res[1];
		for(PII j:Del[i])
			T.update(1,1,cntx,j.first,j.second,-1);
		ans+=ori-T.res[1];
	}
	cout<<ans;
	return 0;
}

P8600 [蓝桥杯 2013 省 B] 连号区间数

对右端点 \(r\) 做扫描线,考虑右端点固定时如何计算合法左端点数量。发现充要条件为 \(r-l=\max-\min\),发现由于是排列,\(\max-\min\geq r-l\),所以 \(\max-\min-r+l\geq 0\)。考虑维护这个东西,\(r\to r+1\) 就是朴素区间加,而改动 \(\max\)\(\min\) 不好处理。考虑使用单调栈维护若干后缀相同的 \(\max\)\(\min\),这样每次修改就是集体区间加。单调栈入栈 + 出栈次数是 \(O(n)\) 的,因此时间复杂度 \(O(n\log n)\)。需要维护全局最小值及其个数。

等效地,CF526F Pudding Monsters

点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=5e4+5;
int n,a[N],stk[2][N],tp[2];
LL ans;
struct Sgt{
	#define ls p<<1
	#define rs p<<1|1
	#define mid ((l+r)>>1)
	int mn[N<<2],cnt[N<<2],tg[N<<2];
	void pushup(int p){
		mn[p]=min(mn[ls],mn[rs]);
		cnt[p]=cnt[ls]*(mn[ls]==mn[p])+cnt[rs]*(mn[rs]==mn[p]);
	}
	void build(int p,int l,int r){
		if(l==r){mn[p]=N,cnt[p]=1;return ;}
		build(ls,l,mid);build(rs,mid+1,r);
		pushup(p);
	}
	void mktag(int p,int v){
		mn[p]+=v;
		tg[p]+=v;
	}
	void pushdown(int p){
		if(tg[p]){
			mktag(ls,tg[p]);
			mktag(rs,tg[p]);
			tg[p]=0;
		}
	}
	void update(int p,int l,int r,int L,int R,int v){
		if(L>R)return ;
		if(L<=l&&r<=R){mktag(p,v);return ;}
		pushdown(p);
		if(L<=mid)update(ls,l,mid,L,R,v);
		if(R>mid)update(rs,mid+1,r,L,R,v);
		pushup(p);
	}
	#undef ls
	#undef rs
	#undef mid
}T;
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	cin>>n;T.build(1,1,n);
	for(int i=1;i<=n;i++){
		cin>>a[i];
		T.update(1,1,n,i,i,-N);
		while(tp[0]&&a[stk[0][tp[0]]]<a[i])
			T.update(1,1,n,stk[0][tp[0]-1]+1,stk[0][tp[0]],-a[stk[0][tp[0]]]+a[i]),tp[0]--;
		while(tp[1]&&a[stk[1][tp[1]]]>a[i])
			T.update(1,1,n,stk[1][tp[1]-1]+1,stk[1][tp[1]],a[stk[1][tp[1]]]-a[i]),tp[1]--;
		if(i>1)T.update(1,1,n,1,i-1,-1);
		stk[0][++tp[0]]=i;stk[1][++tp[1]]=i;
		ans+=T.cnt[1];
	}
	cout<<ans<<'\n';
	return 0;
}

二维数点

前言:\(\text{2 - side}\) 矩形数颜色问题在点数比较少的情况下可以使用扫描线解决,具体请见P7880 [Ynoi2006] rldcot的扫描线解法。

树状数组(离线二维数点)

这个东西就是扫描线的一个 \(\text{subset}\)。考虑把 \(x\) 的值域拎出来单独跑,必要时可以离散化,然后把询问都离线下来,按照 \(x\) 从小到大边加边回答询问即可,用一个数据结构搞 \([l,r]\),时间复杂度 \(O((n+m)\log n)\)

P10814 【模板】离线二维数点

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=2e6+5;
int n,m,a[N];
vector<int>G[N];
struct Tre{
	int av[N];
	int lowbit(int x){return x&-x;}
	void ins(int p,int x){
		for(int i=p;i<=n;i+=lowbit(i))
			av[i]+=x;
	}
	int que(int p){
		int res=0;
		for(int i=p;i;i-=lowbit(i))
			res+=av[i];
		return res;
	}
}T;
int ans[N];
struct Q{int l,r,id,x;}q[N];
bool cmp(Q x,Q y){return x.x<y.x;}
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)
		scanf("%d",&a[i]),
		G[a[i]].push_back(i);
	for(int i=1;i<=m;i++){
		scanf("%d%d%d",&q[i].l,&q[i].r,&q[i].x);
		q[i].id=i;
	}
	sort(q+1,q+1+m,cmp);
	int pos=1;
	for(int i=1;i<=(int)(2e6);i++){
		for(int v:G[i])T.ins(v,1);
		while(pos<=m&&q[pos].x<=i){
			ans[q[pos].id]=T.que(q[pos].r)-T.que(q[pos].l-1);
			pos++;
		}
	}
	for(int i=1;i<=m;i++)
		printf("%d\n",ans[i]);
	return 0;
}

扫描线维护支配点对答案

P7880 [Ynoi2006] rldcot

写得异常顺利。主要需要考虑一个 \(dep_u\) 贡献到答案中当且仅当存在其两个不同子树内的节点(或者 \(u\) 与自己匹配,\(u\) 与其他匹配),那么这两个节点 \(\text{LCA}\) 一定是 \(u\)。考虑固定一个点 \(v_1\),对另一个点 \(v_2\) 考虑,发现只需要考虑到满足条件(即 \(\text{LCA}(v_1,v_2)=u\))的 \(v_2\)点编号离得最近的那两个即可,因为询问区间是连续的。根据 DSU on tree 这样的点对是 \(O(n\log n)\) 的,然后考虑离线二维数点,一个点具有坐标 \((x,y)\) 与颜色 \(c\),其中我们强制 \(x\le y\),那么一个询问 \([l,r]\) 只需要统计有多少 \(x\geq l,y\le r\) 的不同颜色种类数。发现可以在 \(x\) 轴上从右到左扫(\(x\geq l\) 在扫描中满足),然后对每个颜色 \(c\) 开一个 \(pos_c\) 表示当前状态下颜色为 \(c\) 的点 \(y\) 的最小值。发现我们要快速统计 \(\le r\)\(pos_c\) 数量,考虑开一个桶。发现我们需要快速前缀查询,把这个桶变成权值树状数组即可。

时间复杂度 \(O(n\log^2 n+q\log n)\)

点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
typedef pair<int,int> PII;
const int N=5e5+5;
int n,m,idx,head[N],pcnt;
LL dep[N],lsh[N],cnt;
struct Edge{int v,next,w;}e[N<<1];
void ins(int x,int y,int z){
	e[++idx].v=y;
	e[idx].next=head[x];
	e[idx].w=z;
	head[x]=idx;
}
set<int>s[N];
vector<PII>P[N],Q[N];
void dfs0(int u,int fa){
	for(int i=head[u];i;i=e[i].next){
		int v=e[i].v,w=e[i].w;
		if(v==fa)continue;
		dep[v]=dep[u]+w;
		dfs0(v,u);
	}
}
void dfs(int u,int fa){
	s[u].insert(u);
	P[u].emplace_back(u,dep[u]);
	for(int i=head[u];i;i=e[i].next){
		int v=e[i].v;
		if(v==fa)continue;
		dfs(v,u);
		if(s[u].size()<s[v].size())
			swap(s[u],s[v]);
		for(int j:s[v]){
			auto it=s[u].upper_bound(j);
			if(it!=s[u].end())
				P[min(*it,j)].emplace_back(max(*it,j),dep[u]);
			if(it!=s[u].begin()){
				it=prev(it);
				P[min(*it,j)].emplace_back(max(*it,j),dep[u]);
			}
		}
		for(int j:s[v])
			s[u].insert(j);
		s[v].clear();
	}
}
int ans[N],rmn[N];
struct BIT{
	int av[N];
	int lowbit(int x){return x&-x;}
	void ins(int p,int x){
		for(int i=p;i<=n+1;i+=lowbit(i))
			av[i]+=x;
	}
	int que(int p){
		int res=0;
		for(int i=p;i;i-=lowbit(i))
			res+=av[i];
		return res;
	}
}T;
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	cin>>n>>m;
	for(int i=1;i<n;i++){
		int u,v,w;cin>>u>>v>>w;
		ins(u,v,w);ins(v,u,w);
	}
	dfs0(1,0);
	for(int i=1;i<=n;i++)
		lsh[++cnt]=dep[i];
	sort(lsh+1,lsh+1+cnt);
	cnt=unique(lsh+1,lsh+1+cnt)-(lsh+1);
	for(int i=1;i<=n;i++)
		dep[i]=lower_bound(lsh+1,lsh+1+cnt,dep[i])-lsh;
	dfs(1,0);
	for(int i=1;i<=m;i++){
		int l,r;cin>>l>>r;
		Q[l].emplace_back(make_pair(r,i));
	}
	for(int i=1;i<=n;i++)rmn[i]=n+1;
	T.ins(n+1,n);
	for(int i=n;i>=1;i--){
		for(PII j:P[i]){
			if(j.first<rmn[j.second]){
				T.ins(rmn[j.second],-1);
				rmn[j.second]=j.first;
				T.ins(j.first,1);
			}
		}
		for(PII j:Q[i])
			ans[j.second]=T.que(j.first);
	}
	for(int i=1;i<=m;i++)
		cout<<ans[i]<<'\n';
	return 0;
}

P8528 [Ynoi2003] 铃原露露

神秘支配点对问题,板子套板子,想了半天为什么要使用线段树维护历史和。考虑点对 \((a_x,a_y,a_x)\) 在什么时候会找出非法区间,发现默认 \(a_x\le a_y\),当 \(a_z<a_x\)\(l\in(a_z,a_x]\land r\in[a_y,n]\) 的区间 \([l,r]\) 非法,同时 \(a_y<a_z\)\(l\in[1,a_x]\land r\in[a_y,a_z)\) 的区间非法。那么我们要做的事情就是矩形覆盖,然后查询是求为 \(0\) 的点的数量。

我们注意到查询实际上是一个 \(y=x\) 上方的右下 2-side 矩形数点问题,由于 \(a_i\in[1,n]\),可以逐级扫每层,并且对于每层为 \(0\) 的最小值都计入历史最小值及其 \(cnt\) 中。当我们 \(r\) 从小到大扫这个东西时,我们的历史和相当于为我们保存了一段 \(y\le r\) 的前缀信息,而之所以要用历史最小值是因为区别于矩形面积并,我们需要进行区间查询。当然不使用历史最小值,根据矩形面积并的线段树改过来区间查询好像也行,但是仍然要记前缀信息还要另开一个区间加区间查线段树,不如直接在上面做。

于是每次更新前缀 \([1,r]\) 的历史最小值信息达到查 \(y=x\) 上方的效果,然后只有当前节点 \(\min\)\(0\) 才区间加,只有当前节点和儿子节点 \(\min\) 相等才下传最小值加标记即可。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
typedef set<int>::iterator IT;
typedef pair<int,int> PII;
const int N=2e5+5;
int n,m,a[N],fat[N];
LL ans[N];
vector<PII>Que[N],Opt[N];
vector<int>G[N];
set<int>s[N];
void add(int x,int y,int z){
	if(z<x)
		Opt[y].emplace_back(make_pair(z+1,x));
	else if(y<z){
		Opt[y].emplace_back(make_pair(1,x));
		Opt[z].emplace_back(make_pair(-1,-x));
	}
}
void dfs(int u){
	s[u].insert(a[u]);
	for(int v:G[u]){
		dfs(v);
		if(s[u].size()<s[v].size())
			swap(s[u],s[v]);
		for(int i:s[v]){
			IT it=s[u].lower_bound(i);
			if(it!=s[u].end())
				add(i,*it,a[u]);
			if(it!=s[u].begin()){
				it=prev(it);
				add(*it,i,a[u]);
			}
		}
		for(int i:s[v])
			s[u].insert(i);
		s[v].clear();
	}
}
struct Sgt{
	#define ls p<<1
	#define rs p<<1|1
	#define mid ((l+r)>>1)
	int mn[N<<2],cnt[N<<2],tg1[N<<2],tg2[N<<2];
	LL sum[N<<2];
	void pushup(int p){
		mn[p]=min(mn[ls],mn[rs]);
		sum[p]=sum[ls]+sum[rs];
		cnt[p]=cnt[ls]*(mn[ls]==mn[p])+
			cnt[rs]*(mn[rs]==mn[p]);
	}
	void build(int p,int l,int r){
		if(l==r){cnt[p]=1;return ;}
		build(ls,l,mid);
		build(rs,mid+1,r);
		pushup(p);
	}
	void mktag1(int p,int v){
		tg1[p]+=v;
		mn[p]+=v;
	}
	void mktag2(int p,int v){
		tg2[p]+=v;
		sum[p]+=1ll*v*cnt[p];
	}
	void pushdown(int p){
		if(tg1[p]){
			mktag1(ls,tg1[p]);
			mktag1(rs,tg1[p]);
			tg1[p]=0;
		}
		if(tg2[p]){
			if(mn[p]==mn[ls])mktag2(ls,tg2[p]);
			if(mn[p]==mn[rs])mktag2(rs,tg2[p]);
			tg2[p]=0;
		}
	}
	void update1(int p,int l,int r,int L,int R,int v){
		if(L<=l&&r<=R){mktag1(p,v);return ;}
		pushdown(p);
		if(L<=mid)update1(ls,l,mid,L,R,v);
		if(R>mid)update1(rs,mid+1,r,L,R,v);
		pushup(p);
	}
	void update2(int p,int l,int r,int L,int R,int v){
		if(L<=l&&r<=R){if(!mn[p]){mktag2(p,v);}return ;}
		pushdown(p);
		if(L<=mid)update2(ls,l,mid,L,R,v);
		if(R>mid)update2(rs,mid+1,r,L,R,v);
		pushup(p);
	}
	LL query(int p,int l,int r,int L,int R){
		if(L<=l&&r<=R)return sum[p];
		LL res=0;pushdown(p);
		if(L<=mid)res+=query(ls,l,mid,L,R);
		if(R>mid)res+=query(rs,mid+1,r,L,R);
		return res;
	}
	#undef ls
	#undef rs
	#undef mid
}T;
int main(){
	//freopen("ynoi.in","r",stdin);
	//freopen("ynoi.out","w",stdout);
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	cin>>n>>m;
	for(int i=1;i<=n;i++)cin>>a[i];
	for(int i=2;i<=n;i++)cin>>fat[i],
		G[fat[i]].emplace_back(i);
	for(int i=1;i<=m;i++){
		int l,r;cin>>l>>r;
		Que[r].emplace_back(make_pair(l,i));
	}
	dfs(1);
	T.build(1,1,n);
	for(int i=1;i<=n;i++){
		for(PII v:Opt[i]){
			int l=v.first,r=v.second,d=1;
			if(l<0)l=-l,r=-r,d=-d;
			T.update1(1,1,n,l,r,d);
		}
		T.update2(1,1,n,1,i,1);
		for(PII v:Que[i]){
			int l=v.first,r=i,id=v.second;
			ans[id]=T.query(1,1,n,l,r);
		}
	}
	for(int i=1;i<=m;i++)cout<<ans[i]<<'\n';
	return 0;
}

P13342 [EGOI 2025] Wind Turbines / 风力涡轮机

结合 Kruskal 重构树食用。一步转换后可以发现这是一个在原最小生成树上找到 \([l,r]\) 所有点中两两联通路径上断 \(r-l\) 条最大边权边,由最小生成树最大边权联想到 Kruskal 重构树,发现只需要在上面找两两 \(\text{LCA}\) 的前 \(r-l\) 大,而这正好是 Kruskal 重构树 \(r-l+1\) 个叶子节点的公共祖先数目。

考虑求出这个东西,对于每个公共祖先考虑其贡献点对 \((v_1,v_2)\)(叶子节点,\(v_1<v_2\)),其中 \(v_1\in tree(s_1),v_2\in tree(s_2)\)\(s_1,s_2\) 分别为 \(u\) 的左右儿子。由于询问区间的连续性,考虑找支配点对这一经典 trick。具体来说,子树中 \(v_1\) 匹配的 \(v_2\) 只需要找到跨子树且与其最近的最多两个 \(v_2\) 作为有效点对,根据 DSU on tree 点对数量 \(O(n\log n)\),用启发式合并实现时间复杂度 \(O(n\log n)\)

对于询问这是一个 \((v_1,v_2)\) 贡献的左上 2-side 矩形数点(带权)问题。考虑到一个 \(u\) 只能被统计一次,记 \(u\) 出现的最小 \((v_1,v_2)\) 中的 \(v_2\)\(l\)\(n\)\(1\) 扫。每次如果可以更新该值就在树状数组上单点改,查询区间查即可。时间瓶颈在于树上启发式合并。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
typedef pair<int,int> PII;
typedef set<int>::iterator IT;
const int N=2e5+5;
int n,m,Q,fa[N],ncnt,all;
int ecnt,val[N],mn[N];
LL sum,ans[N];
struct Edge{int u,v,w;}e[N],E[N];
vector<int>G[N];
vector<PII>Add[N],Que[N];
set<int>s[N];
bool cmp(Edge x,Edge y){return x.w<y.w;}
inline int fr(int x){return fa[x]==x?x:fa[x]=fr(fa[x]);}
struct BIT{
	LL av[N];
	inline int lowbit(int x){return x&-x;}
	void add(int p,LL x){for(int i=p;i<=n+1;i+=lowbit(i))av[i]+=x;}
	LL que(int p){LL res=0;for(int i=p;i;i-=lowbit(i)){res+=av[i];}return res;}
}T;
bool ins(int x,int y){
	int frx=fr(x),fry=fr(y);
	if(frx==fry)return 0;
	fa[frx]=fry;
	return 1;
}
void dfs(int u){
	if(u<=n)s[u].insert(u);
	for(int v:G[u]){
		dfs(v);
		if(s[v].size()>s[u].size())swap(s[u],s[v]);
		for(int i:s[v]){
			IT suf=s[u].upper_bound(i);
			if(suf!=s[u].end())Add[i].emplace_back(make_pair(*suf,u));
			if(suf!=s[u].begin()){
				IT pre=prev(suf);
				Add[*pre].emplace_back(make_pair(i,u));
			}
		}
		for(int i:s[v])s[u].insert(i);
		s[v].clear();
	}
}
int main(){
	//freopen("wind.in","r",stdin);
	//freopen("wind.out","w",stdout);
	scanf("%d%d%d",&n,&m,&Q);ncnt=n;
	for(int i=1;i<=n;i++)
		fa[i]=i;
	all=n;
	for(int i=1;i<=m;i++){
		int u,v,w;scanf("%d%d%d",&u,&v,&w);
		u++;v++;
		e[i]=Edge{u,v,w};
	}
	sort(e+1,e+1+m,cmp);
	for(int i=1;i<=m;i++)
		if(ins(e[i].u,e[i].v))
			E[++ecnt]=e[i],sum+=e[i].w;
	for(int i=1;i<=2*n;i++)
		fa[i]=i;
	for(int i=1;i<=ecnt;i++){
		int fru=fr(E[i].u),frv=fr(E[i].v);
		val[++ncnt]=E[i].w;
		ins(fru,ncnt);ins(frv,ncnt);
		G[ncnt].emplace_back(fru);
		G[ncnt].emplace_back(frv);
	}
	dfs(ncnt);
	for(int i=1;i<=Q;i++){
		int l,r;scanf("%d%d",&l,&r);
		l++;r++;
		Que[l].emplace_back(make_pair(r,i));
	}
	for(int i=n+1;i<=ncnt;i++)
		mn[i]=n+1;
	T.add(n+1,ncnt-n);
	for(int i=n;i>=1;i--){
		for(PII v:Add[i]){
			int u=v.second;
			if(v.first<mn[u]){
				T.add(mn[u],-val[u]);
				mn[u]=v.first;
				T.add(mn[u],val[u]);
			}
		}
		for(PII v:Que[i])
			ans[v.second]=T.que(v.first);
	}
	for(int i=1;i<=Q;i++)
		printf("%lld\n",sum-ans[i]);
	return 0;
}

P11882 [RMI 2024] 彩虹糖 / Skittlez(在线二维数点)

绝对众数经典 trick,考虑二进制分组直接确定 \((x,y)\) 的区间众数二进制上每一位是否为 \(1\),得出的 \(ans\) 如果合法直接二维数点。注意到我们无法对每个 \(i=1\to q\) 都做一次二维前缀和,但是可以用在线二维数点优化这个过程,使用一个二维树状数组即可。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=1e3+5,M=5e5+5;
int n,q;
struct Tre{
	LL av[N][N];
	int lowbit(int x){return x&-x;}
	void ins(int x,int y,LL v){
		for(int i=x;i<=n;i+=lowbit(i))
			for(int j=y;j<=n;j+=lowbit(j))
				av[i][j]+=v;
	}
	LL que(int x,int y){
		LL res=0;
		for(int i=x;i;i-=lowbit(i))
			for(int j=y;j;j-=lowbit(j))
				res+=av[i][j];
		return res;
	}
}T;
LL t1[N][N][20],val[N][N];
int ans[N][N];
struct Q{int xa,ya,xb,yb,k;};
vector<Q>vec[M];
vector<pair<int,int> >P[M];
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	cin>>n>>q;
	for(int i=1;i<=q;i++){
		int xa,ya,xb,yb,c,k;
		cin>>xa>>ya>>xb>>yb>>c>>k;
		val[xa][ya]+=k;
		val[xb+1][ya]-=k;
		val[xa][yb+1]-=k;
		val[xb+1][yb+1]+=k;
		vec[c].push_back((Q){xa,ya,xb,yb,k});
		for(int j=0;j<=19;j++){
			if((c>>j)&1){
				t1[xa][ya][j]+=k;
				t1[xb+1][ya][j]-=k;
				t1[xa][yb+1][j]-=k;
				t1[xb+1][yb+1][j]+=k;
			}
		}
	}
	for(int i=1;i<=n;i++)
		for(int j=1;j<=n;j++)
			val[i][j]+=-val[i-1][j-1]+val[i-1][j]+val[i][j-1];
	for(int k=0;k<=19;k++){
		for(int i=1;i<=n;i++)
			for(int j=1;j<=n;j++){
				t1[i][j][k]+=-t1[i-1][j-1][k]+t1[i-1][j][k]+t1[i][j-1][k];
				if(t1[i][j][k]*2>val[i][j])ans[i][j]|=(1<<k);
				else if(t1[i][j][k]*2==val[i][j])ans[i][j]=-1;
			}
	}
	for(int i=1;i<=n;i++)
		for(int j=1;j<=n;j++){
			if(ans[i][j]>=1&&ans[i][j]<=q)
				P[ans[i][j]].push_back(make_pair(i,j));
		}
	for(int i=1;i<=q;i++){
		if(!P[i].size())continue;
		if(!vec[i].size()){
			for(auto j:P[i]){
				int x=j.first,y=j.second;
				ans[x][y]=-1;
			}
			continue;
		}
		for(Q j:vec[i]){
			int xa=j.xa,ya=j.ya,xb=j.xb,yb=j.yb,k=j.k;
			T.ins(xa,ya,k);T.ins(xa,yb+1,-k);
			T.ins(xb+1,ya,-k);T.ins(xb+1,yb+1,k);
		}
		for(auto j:P[i]){
			int x=j.first,y=j.second;
			if(T.que(x,y)*2<=val[x][y])
				ans[x][y]=-1;
		}
		for(Q j:vec[i]){
			int xa=j.xa,ya=j.ya,xb=j.xb,yb=j.yb,k=j.k;
			T.ins(xa,ya,-k);T.ins(xa,yb+1,k);
			T.ins(xb+1,ya,k);T.ins(xb+1,yb+1,-k);
		}
	}
	for(int i=1;i<=n;i++){
		for(int j=1;j<=n;j++){
			if(ans[i][j]<=0||ans[i][j]>q)cout<<'-'<<'1'<<' ';
			else cout<<ans[i][j]<<' ';
		}
		cout<<'\n';
	}
	return 0;
}

线段树(离线二维数点)

有时候仅凭树状数组无法解决一些问题,如一些不具有可差分性的信息,这时候就可以采用线段树维护啦啦啦。

P11364 [NOIP2024] 树上查询

首先,区间 \(\text{LCA}\) 的深度为:

\[\min_{l\le i<r}{\text{dep}_{\text{LCA}(i,i+1)}} \]

可以用虚树的方法证。
我们找出以 \(\text{LCA}(i,i+1)\) 为最近公共祖先的最大区间 \([x_i,y_i,v_i]\)\(v_i\)\(\text{dep}_{\text{LCA}(i,i+1)}\)
显然,查询是求与 \([l,r]\) 交集至少为 \(k\),且最大的 \(v_i\)。可列出两个不等式。

\[y_i\ge r\land x_i\le r-k+1 \\ l+k-1\le y_i\le r\land y_i-x_i+1\ge k \]

第一个对 \(r\) 扫描线,第二个对 \(k\) 扫描线,时间复杂度 \(O(n\log n)\)
——Yonder题解:P11364 [NOIP2024] 树上查询

这个区间交集划分的方法妙处就在于把原本需要划分 4 中情况的讨论压缩为了两种,大大减少了讨论情况种类与要写 CDQ 求解三维偏序的可能,可以说这个区间交集推导就是本题的精华。

需要注意的是,题目中存在 \(k=1\) 的情况,因此可以直接搞一个 st 表维护。主播太懒了于是直接在 \([x_i,y_i,v_i]\) 中加入了 \([i,i,\text{dep}_i]\),但是这样做常数是两倍的,\(2n\) 达到了 \(10^6\),带 \(\log\) 比较容易 TLE,所以还是单独写一个吧。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=5e5+5;
int n,qn,dep[N],f[N][21],V[N],X[N],Y[N];
int stk[N],tp,ans[N],cnt;
vector<int>G[N];
struct Q{int l,r,k,id;}q[N];
struct P{int x,y,v;}op[N*2];
void dfs(int u,int fa){
	dep[u]=dep[fa]+1;
	for(int v:G[u]){
		if(v==fa)continue;
		f[v][0]=u;
		for(int j=1;(1<<j)<=dep[u];j++)
			f[v][j]=f[f[v][j-1]][j-1];
		dfs(v,u);
	}
}
int LCA(int x,int y){
	if(dep[x]<dep[y])swap(x,y);
	for(int i=20;i>=0;i--)
		if(dep[f[x][i]]>=dep[y])
			x=f[x][i];
	if(x==y)return x;
	for(int i=20;i>=0;i--)
		if(f[x][i]!=f[y][i])
			x=f[x][i],y=f[y][i];
	return f[x][0];
}
bool cmp1(Q x,Q y){return x.r>y.r;}
bool cmp2(P x,P y){return x.y>y.y;}
bool cmp3(Q x,Q y){return x.k>y.k;}
bool cmp4(P x,P y){return x.y-x.x>y.y-y.x;}
struct Sgt{
	#define ls p<<1
	#define rs p<<1|1
	#define mid ((l+r)>>1)
	int mx[N<<2],Ln[N<<2],Rn[N<<2];
	void pushup(int p){mx[p]=max(mx[ls],mx[rs]);}
	void build(int p,int l,int r){
		Ln[p]=l,Rn[p]=r;
		if(l==r){mx[p]=0;return ;}
		build(ls,l,mid);
		build(rs,mid+1,r);
		pushup(p);
	}
	void modify(int p,int pos,int val){
		int l=Ln[p],r=Rn[p];
		if(l==r){mx[p]=max(mx[p],val);return ;}
		if(pos<=mid)modify(ls,pos,val);
		else modify(rs,pos,val);
		pushup(p);
	}
	int query(int p,int L,int R){
		int l=Ln[p],r=Rn[p];
		if(L<=l&&r<=R)return mx[p];
		if(L<=mid&&R>mid)return max(query(ls,L,R),query(rs,L,R));
		if(L<=mid)return query(ls,L,R);
		return query(rs,L,R);
	}
	#undef ls
	#undef rs
	#undef mid
}T;
int main(){
	scanf("%d",&n);
	for(int i=1;i<n;i++){
		int u,v;scanf("%d%d",&u,&v);
		G[u].push_back(v);G[v].push_back(u);
	}
	dfs(1,0);
	tp=0;
	for(int i=1;i<n;i++){
		V[i]=dep[LCA(i,i+1)];
		while(tp&&V[stk[tp]]>=V[i])tp--;
		if(tp)X[i]=stk[tp]+1;
		else X[i]=1;
		stk[++tp]=i;
	}
	tp=0;
	for(int i=n-1;i>=1;i--){
		while(tp&&V[stk[tp]]>=V[i])tp--;
		if(tp)Y[i]=stk[tp];
		else Y[i]=n;
		stk[++tp]=i;
		op[++cnt]=(P){X[i],Y[i],V[i]};
	}
	scanf("%d",&qn);
	for(int i=1;i<=qn;i++)
		scanf("%d%d%d",&q[i].l,&q[i].r,&q[i].k),q[i].id=i;
	sort(q+1,q+1+qn,cmp1);
	sort(op+1,op+cnt+1,cmp2);
	int pos=1;
	T.build(1,1,n);
	for(int i=1;i<=qn;i++){
		int l=q[i].l,r=q[i].r,k=q[i].k;
		while(pos<=cnt&&op[pos].y>=r){
			T.modify(1,op[pos].x,op[pos].v);
			pos++;
		}
		ans[q[i].id]=T.query(1,1,r-k+1);
	}
	sort(q+1,q+1+qn,cmp3);
	sort(op+1,op+cnt+1,cmp4);
	pos=1;
	T.build(1,1,n);
	for(int i=1;i<=qn;i++){
		int l=q[i].l,r=q[i].r,k=q[i].k;
		while(pos<=cnt&&op[pos].y-op[pos].x+1>=k){
			T.modify(1,op[pos].y,op[pos].v);
			pos++;
		}
		ans[q[i].id]=max(ans[q[i].id],T.query(1,l+k-1,r));
	}
	for(int i=1;i<=qn;i++)printf("%d\n",ans[i]);
	return 0;
}

CF377D Developing Game

转化题目条件,选一堆人的能力区间必定有交,且交包含所有 \(v_i\),即对于 \(S=\bigcap_{i=1}^k[l_{a_i},r_{a_i}]\),有 \(\forall v_{a_i}\in S\)。考虑枚举 \(S=[L,R]\),发现一个人能贡献到交区间的充要条件是 \(l_i\le L\le v_i\le R\le r_i\),这是一个经典离线二维数点问题,对点 \((L,R)\) 进行覆盖统计,数数的时候线段树/树状数组暴力找最大值即可。

平替方法:可持久化线段树

不包括不可差分信息的离线二维数点中,直接用一棵可持久化线段树就可以完成离线二维数点。

posted @ 2025-05-26 15:02  TBSF_0207  阅读(74)  评论(0)    收藏  举报