【DS】CDQ 分治学习笔记
都什么年代了还在做传统分治(×
0x01:引入
CDQ 分治是一类离线算法,一般用来计算形如点对 对答案的贡献,其中 满足某种大小限制条件(又称偏序关系)。显然这样的点对共有 个,CDQ 分治则能够在(一般是) ( 为限制个数) 的时间中解决。
0x02:流程
以 P3810 【模板】三维偏序(陌上花开) 为例。
我们要计算点对 的数量,满足 。
假设所有元素都互不相同,先以 为第一关键字做三关键字排序。则对于所有 ,满足条件的 一定在其左侧。
对整个序列进行关于第二维 的双关键字归并排序,把所有可贡献的 分成三类:
前两类可以在子分治归并中计算,考虑计算第三类情况:显然,子分治归并后,对 有贡献的 是 的一个前缀,且随着 的移动单调(因为对左右两区间的 单调),于是可以维护双指针,对于 用树状数组维护并查询即可。
注意为什么上面假设所有元素互不相同,因为 CDQ 一般解决不了有重复元素的问题,除非重复元素之间不算贡献,所以要去重。
#include<cstdio>
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
template <typename T>
inline void read(T& r) {
r=0;bool w=0;
char ch=getchar();
while(ch<'0'||ch>'9') w=ch=='-'?1:0,ch=getchar();
while(ch>='0'&&ch<='9') r=r*10+(ch-'0'), ch=getchar();
r=w?-r:r;
}
const int N=1e5+10;
#define lb x&-x
int n,k,f[N];
struct node{
int a,b,c;
int cnt,ans;
}a[N],b[N];
int tre[N<<1];
inline bool cmp1(const node& x,const node& y){
return x.a==y.a?(x.b==y.b?x.c<y.c:x.b<y.b):x.a<y.a;
}
inline bool cmp2(const node& x,const node& y){
return x.b==y.b?x.c<y.c:x.b<y.b;
}
void add(int x,int v){
for(;x<=k;x+=lb)tre[x]+=v;
}
int query(int x){
int res=0;
for(;x;x-=lb)res+=tre[x];
return res;
}
void solve(int l,int r){
if(l==r)return;
int mid=l+r>>1;
solve(l,mid),solve(mid+1,r);
sort(a+l,a+mid+1,cmp2),sort(a+mid+1,a+r+1,cmp2);//偷懒换来大常数
int j=l-1;
for(int i=mid+1;i<=r;++i){
while(a[j+1].b<=a[i].b&&j+1<=mid)++j,add(a[j].c,a[j].cnt);
a[i].ans+=query(a[i].c);
}
for(int i=l;i<=j;++i)add(a[i].c,-a[i].cnt);
}
int main(){
read(n),read(k);
for(int i=1;i<=n;++i)read(b[i].a),read(b[i].b),read(b[i].c);
sort(b+1,b+n+1,cmp1);
int tot=0,cnt=0;
for(int i=1;i<=n;++i){
++cnt;
if(b[i].a!=b[i+1].a||b[i].b!=b[i+1].b||b[i].c!=b[i+1].c){
a[++tot]=b[i];
a[tot].cnt=cnt;cnt=0;
}
}
solve(1,tot);
for(int i=1;i<=tot;++i)f[a[i].ans+a[i].cnt-1]+=a[i].cnt;
for(int i=0;i<n;++i)printf("%d\n",f[i]);
return 0;
}
总的来说,CDQ 分治应该是一个通过排序不断降维的过程,将限制条件变简单后加上数据结构维护。
CDQ 分治大多数用来解决三维偏序问题,三维以上很少见;二维其实就是对第一维排序转化成逆序对问题;一维?排个序不就行了。
于是一般问题的解决思路就是,转化成三维偏序问题,然后 CDQ 计算贡献。
一些注意的点:
- 是离线!!!
- 注意是否要离散化。
- 上面已经讲过了,有重复元素且能相互贡献的话要去重。
- 注意排序时要多关键字,才能保证满足条件的 一定在 左边。
- 有待补充
0x03:例题
题意:给你二维坐标系上的点,每个点有权值,每次询问一个矩形内的点权和。
思路:KDT?我不会,考虑怎么转化成 CDQ 分治。
显然可以二维前缀和,一个查询转化成了多个子问题:求一个点左下方矩形的权值和,这太好做了。
对于一个点 ,它左下方的权值和就是 。
当然,询问的点和原来的点要区分开啊,再多加一维 , 表示原来的点, 表示查询点,则对于一个查询点 它左下方的权值和为 。这就成了三维偏序问题, 解决。
注意到 只有两种取值,故不用数据结构维护,简单归并一下就是 。
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
typedef long long ll;
const int N=5e5+10;
int n,m,tot=0;
struct Data{int x,y,z,w,id,op;ll sum=0;}q[N],tmp[N];
ll ans[N];
inline bool cmp1(Data a,Data b){
return a.x==b.x?(a.y==b.y?a.z<b.z:a.y<b.y):a.x<b.x;
}
void cdq(int l,int r){
if(l==r)return;
int mid=(l+r)>>1;
cdq(l,mid),cdq(mid+1,r);
int i=l,j=mid+1,k=1;ll sum=0;
while(i<=mid&&j<=r){
if(q[i].y<=q[j].y)sum+=!q[i].z*q[i].w,tmp[k++]=q[i++];
else ans[q[j].id]+=sum*q[j].op,tmp[k++]=q[j++];
}
while(i<=mid)tmp[k++]=q[i++];
while(j<=r)ans[q[j].id]+=sum*q[j].op,tmp[k++]=q[j++];
for(i=l,j=1;i<=r;++i,++j)q[i]=tmp[j];
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;++i){
int x,y,w;
scanf("%d%d%d",&x,&y,&w);
q[++tot]=(Data){x,y,0,w};
}
for(int i=1;i<=m;++i){
int x1,x2,y1,y2;
scanf("%d%d%d%d",&x1,&y1,&x2,&y2);
q[++tot]=(Data){x2,y2,1,0,i,1};
q[++tot]=(Data){x2,y1-1,1,0,i,-1};
q[++tot]=(Data){x1-1,y2,1,0,i,-1};
q[++tot]=(Data){x1-1,y1-1,1,0,i,1};
}
sort(q+1,q+tot+1,cmp1);
cdq(1,tot);
for(int i=1;i<=m;++i)printf("%lld\n",ans[i]);
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】