CDQ分治
CDQ
思想概述-基于离线的分治算法
首先明确几个概念:
动态问题:即带有修改操作的问题
静态问题:即无修改操作的问题
当一个问题是动态问题并且并不强制在线的,我们可以进行离线操作——对问题的某一维度进行分治
一般来说,CDQ分治分为三步
- 统计分治的前段区间
的修改操作对 中查询操作的影响 - 递归解决区间
和
当然,我们并不要求统计一定要在递归解决子区间前面,什么时候方便统计影响就什么时候统计
很显然的是,若我们按照时间轴进行分治计算一个动态问题,这个算法的正确性就很显然了,具体详见《算法竞赛进阶指南》
而,统计影响便是一个静态问题,因为我们保证了所有的修改都在查询之前。所以说,CDQ算法本质上是:
将动态问题离线化,对时间轴进行分治,达到将动态问题转变为若干个静态问题的目的
很明显,静态问题显然更好解决
一般来说,若能够保证解决区间
下面通过三道题来感受CDQ分治的魅力
经典例题
- 动态二维前缀和问题:莫基亚
题目描述
第一行两个整数 S,W,其中 S 为矩阵初始值,W 为矩阵大小。
接下来每行为以下三种输入之一:
“1 x y a”——把第 x 行第 y 列的格子 (x,y) 权值增加 a;
“2 x1 y1 x2 y2”——询问以 (x1,y1) 为左下角,(x2,y2) 为右上角的矩阵内所有格子的权值和;
“3”——输入结束。
S始终为0
好的下面我们将会使用CDQ分治算法解决这道题
使用CDQ分治转变为静态问题的话,我们的问题就变成了:
给定
给定两个数组
这个问题实质上是二维偏序的一个变式,按照套路,我们以一维树状数组和排序解决,具体的:
- 设将
放于一个数组 中,按照 进行递增排序 - 从小到大扫描每一个
,进行分类讨论 - 若
原属于 (即 的操作编号小于等于分治值 ),则将树状数组中 的位置加上 的权值 - 若
原属于 ,则对于树状数组求 即为 的权值和
这个做法的正确性证明:
这个做法,等同于对于树状数组的每一个值,都变成了维护一个向量的和(也即每一个y坐标维护的是平面直角坐标系中那一条直线上的权值和),因为有着x的递增排序在,于是我们每一次统计矩阵和的时候,每一个向量的最大
这样的做法复杂度已经非常可观了,但我们还可以继续优化!很明显的,对于统计这个信息,我也可以在递归解决子区间之前统计,而对于时间轴的分治,我们其实并不如何关心它一定得有严格单调顺序,只需要保证左边区间的所有操作的时间轴小于右边即可,那么我们也就可以将对x的排序放在CDQ分治之前,然后在分治过程中,将时间轴按照一种类似于逆归并排序的思想,将每一步的时间分割成
综上所述,代码如下:有一些设计上的技巧
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
int n,o,tot=0,cnt=0,ans[2000005],c[2000005];
struct node {
int x,y,z,op,id;
bool operator<(const node b)const {
return x<b.x||(x==b.x&&(y<b.y||(y==b.y&op<b.op)));
}
}a[2000005],b[2000005];
void add(int x,int y){
while(x<=n){
c[x]+=y;
x+=x&-x;
}
}
int ask(int x){
int ans=0;
while(x){
ans+=c[x];
x-=x&-x;
}
return ans;
}
void CDQ(int l,int r){
if(l==r)return;
int mid=(l+r)>>1,lt=l,rt=mid+1;
for(int i=l;i<=r;i++){
if(a[i].id<=mid&&!a[i].op)add(a[i].y,a[i].z);
else if(a[i].id>mid&&a[i].op)ans[a[i].op]+=ask(a[i].y)*a[i].z;
}
for(int i=l;i<=r;i++){
if(a[i].id<=mid&&!a[i].op)add(a[i].y,-a[i].z);
}
for(int i=l;i<=r;i++){
if(a[i].id<=mid)b[lt++]=a[i];
else b[rt++]=a[i];
}
for(int i=l;i<=r;i++)a[i]=b[i];
CDQ(l,mid);
CDQ(mid+1,r);//这里必须自顶向下,因为如果自底向上就破坏了x的有序性
}
int main(){
scanf("%d%d",&n,&n);
while(scanf("%d",&o)&&o!=3)
if(o==1){
a[++tot].op=0;
a[tot].id=tot;
scanf("%d %d %d",&a[tot].x,&a[tot].y,&a[tot].z);
}
else {
int x1,y1,x2,y2;
scanf("%d %d %d %d",&x1,&y1,&x2,&y2);
a[++tot]={--x1,--y1,1,++cnt,tot};
a[++tot]={x1,y2,-1,cnt,tot};
a[++tot]={x2,y1,-1,cnt,tot};
a[++tot]={x2,y2,1,cnt,tot};
}
sort(a+1,a+tot+1);
CDQ(1,tot);
for(int i=1;i<=cnt;i++)printf("%d\n",ans[i]);
return 0;
}
- 陌上花开:三维偏序问题
有
对于
考虑如何做此题:
首先,此题非常坑人,需要去重,并且去重之后因为
其次,我们按照降维的方式来考虑
先按照
下面的两个维度使用CDQ分治的思想
在每一次CDQ分治cdq(1,mid);cdq(mid+1,r);
后,可以保证的是[l,mid]
中所有的[mid+1,r]
中的
下面我们思考第二个条件
将两个区间各自按照
代码如下:
#define maxn 100010
#define maxk 200010
#define ll long long
using namespace std;
inline int read(){
int x=0,f=1;
char ch=getchar();
while(isdigit(ch)==0 && ch!='-')ch=getchar();
if(ch=='-')f=-1,ch=getchar();
while(isdigit(ch))x=x*10+ch-'0',ch=getchar();
return x*f;
}
inline void write(int x){
int f=0;char ch[20];
if(!x){puts("0");return;}
if(x<0){putchar('-');x=-x;}
while(x)ch[++f]=x%10+'0',x/=10;
while(f)putchar(ch[f--]);
putchar('\n');
}
typedef struct node{
int x,y,z,ans,w;
}stnd;
stnd a[maxn],b[maxn];
int n,cnt[maxk];
int k,n_;
bool cmpx(stnd u,stnd v){
if(u.x==v.x){
if(u.y==v.y)
return u.z<v.z;
return u.y<v.y;
}
return u.x<v.x;
}
bool cmpy(stnd u,stnd v){
if(u.y==v.y)
return u.z<v.z;
return u.y<v.y;
}
struct treearray{
int tre[maxk],kk;
int lwbt(int x){return x&(-x);}
int ask(int i){int ans=0; for(;i;i-=lwbt(i))ans+=tre[i];return ans;}
void add(int i,int k){for(;i<=kk;i+=lwbt(i))tre[i]+=k;}
}t;
void cdq(int l,int r){
if(l==r)return;
int mid=(l+r)>>1;
cdq(l,mid);cdq(mid+1,r);
sort(a+l,a+mid+1,cmpy);
sort(a+mid+1,a+r+1,cmpy);
int i=mid+1,j=l;
for(;i<=r;i++){
while(a[j].y<=a[i].y && j<=mid)
t.add(a[j].z,a[j++].w);
a[i].ans+=t.ask(a[i].z);
}
for(i=l;i<j;i++)
t.add(a[i].z,-a[i].w);
}
int main(){
n_=read(),k=read();t.kk=k;
for(int i=1;i<=n_;i++)
b[i].x=read(),b[i].y=read(),b[i].z=read();
sort(b+1,b+n_+1,cmpx);
int c=0;
for(int i=1;i<=n_;i++){
c++;
if(b[i].x!=b[i+1].x || b[i].y!=b[i+1].y || b[i].z!=b[i+1].z )
a[++n]=b[i],a[n].w=c,c=0;
}
cdq(1,n);
for(int i=1;i<=n;i++)
cnt[a[i].ans+a[i].w-1]+=a[i].w;
for(int i=0;i<n_;i++)
write(cnt[i]);
return 0;
}
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战