cdq 分治学习笔记

P3810 【模板】三维偏序 / 陌上花开

问题概述: 有 \(n\) 个元素,每个元素有三个属性 \((a, b, c)\),问有多少对 \((i, j)\)( \(j ≠ i\),且按属性两两可比)满足 \(a_j ≤ a_i、b_j ≤ b_i、c_j ≤ c_i\)。

解题思路

第一步:排序,干掉第一维

把元素按 \(a\) 升序排好。这一排,就带来一个极强的性质:

只要 \(i\) 在左半边、\(j\) 在右半边,就一定有 \(a_i ≤ a_j\) —— 第一维的条件自动满足。
第一维不再是"条件",而变成了"方向":贡献只会从左边流向右边。于是问题从三维变成二维。

第二步:分治,把点对分类

写出 solve(l, r),表示统计下标在 \([l, r]\) 内、且跨越 \(mid\) 的点对贡献。取中点 \(mid\),所有 \(i < j\) 的点对被切成三类:
类别 范围 处理方式
i. \(l ≤ i < j ≤ mid\) 都在左半 → 递归 solve(l, mid)
ii. \(l ≤ i ≤ mid < j ≤ r\) 跨越中点 → 本层亲手处理
iii. \(mid < i < j ≤ r\) 都在右半 → 递归 solve(mid+1, r)

i、iii 直接扔给递归,本层只需要集中精力解决 ii。这就是分治的标准节奏:分而治之,只处理"跨界"的部分。

第三步:双指针 + 树状数组,干掉第二、三维

现在只剩二维了:在左半中找满足 \(b_i ≤ b_j\) 且 \(c_i ≤ c_j\) 的 \(i\)。做法非常直白:

把左半 \([l, mid]\) 和右半 \([mid+1, r]\) 分别按 \(b\) 升序排序。
用一个指针 \(i\) 从左半头部开始,依次枚举右半的 \(j\):
把所有 \(b_i ≤ b_j\) 的左半点全部塞进一棵以 \(c\) 为下标的树状数组里,然后指针后移。
此时树状数组里的点全都满足 \(b_i ≤ b_j\),于是只要查询 \(c ≤ c_j\) 的个数(前缀和),就是 \(j\) 收到的贡献。
本层处理完后,把树状数组清空回滚,别影响其他层。
因为 \(b\) 是有序的,指针 \(i\) 只会一路向右走,插入总次数是 \(\mathcal{O(n)}\) 而不是 \(\mathcal{O(n²)}\)。这就是双指针的作用。

容易被忽略的细节

  • cnt:完全相同的点如果不去重,会重复计算。可以先按 \((a,b,c)\) 排序,把相同的点合并成一个,用 cnt 记录出现了几次
  • 离散化:如果 \(c\) 的取值范围远大于 \(n\),先把 \(c\) 压缩到 \(\le n\),树状数组才开得下.当然本题这里没有要求.
代码
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
const int M=2e5+5;
int n,k;
struct abc{
	int a,b,c,ans,cnt;
}e[N],p[N];
int ans[N],tot;
int c[M];
#define lowbit(x) ((x)&-(x))
void add(int i,int x){
	for(;i<=k;i+=lowbit(i)){c[i]+=x;}
}
int ask(int i){
	int res=0;
	for(;i;i-=lowbit(i)){res+=c[i];}
	return res;
}
bool cmp1(abc a,abc b){
	if(a.a!=b.a)return a.a<b.a;
	if(a.b!=b.b)return a.b<b.b;
	return a.c<b.c;
}
bool cmp2(abc a,abc b){
	return a.b<b.b;
}
void solve(int l,int r){
	if(l>=r)return ;
	int mid=(l+r)>>1;
	solve(l,mid);solve(mid+1,r);
	sort(p+l,p+mid+1,cmp2);
	sort(p+mid+1,p+r+1,cmp2);
	int i=l;
	for(int j=mid+1;j<=r;j++){
		while(i<=mid&&p[i].b<=p[j].b){
			add(p[i].c,p[i].cnt);i++;
		}
		p[j].ans+=ask(p[j].c);
	}
	for(int t=l;t<i;t++) add(p[t].c,-p[t].cnt);
}

int main(){
	scanf("%d%d",&n,&k);
	for(int i=1;i<=n;i++) {
		scanf("%d%d%d",&e[i].a,&e[i].b,&e[i].c);
	}
	sort(e+1,e+n+1,cmp1);
	for(int i=1;i<=n;i++){
		abc tmp=e[i];tmp.cnt=1;
		while(i<n&&e[i].a==e[i+1].a&&e[i].b==e[i+1].b&&e[i].c==e[i+1].c){i++;tmp.cnt++;}
		p[++tot]=tmp;
	}
	solve(1,tot);
	for(int i=1;i<=tot;i++){
		ans[p[i].ans+p[i].cnt-1]+=p[i].cnt;
	}
	for(int i=0;i<n;i++) printf("%d\n",ans[i]);
	return 0;
}

数点point

本题也可以用树套树解决

解题思路
代码(cdq分治)

posted @ 2026-09-16 20:35  dgcsjfs  阅读(13)  评论(4)    收藏  举报