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分治)

浙公网安备 33010602011771号