【学习笔记】cdq分治
一. cdq分治的定义
cdq 分治是一种思想,由陈丹琦引入。
-
cdq 分治解决和点对有关的问题
-
cdq 分治优化 1D/1D 动态规划的转移
-
通过 cdq 分治,将一些动态问题转化为静态问题
仅支持离线操作。
一般来说,cdq 分治是通过如下结构进行分治:
分治一共分为四步:
-
找到当前区间 中点 。
-
递归处理左子区间 。
-
递归处理右子区间 。
-
处理左区间对于右区间的影响,并对于右区间或者答案进行更改与修正。
二. 从二维偏序到三维偏序
求逆序对。可以当成二维偏序。
。
二位偏序就是类似于逆序对对吧。
这个显然可以用树状数组做,我们考虑怎么用 cdq 分治做。
类似于归并排序的思想,考虑一直将序列二分下去,然后统计 区间对于 区间带来的贡献。
考虑双指针 分别表示当前走到的左区间的位置和右区间的位置。
因为我们是通过递归,所以显然 都已经是有序的了。
那么通过双指针的移动来确定贡献。具体操作如下:
- 若 ,则不是逆序对,不产生贡献。考虑将 加入一个序列 中。然后继续移动 。
- 若 ,则产生逆序对,发生贡献,注意到 是单调的,所以如果 ,那么对于 ,必然有 ,所以 加上 ,考虑将 加入 。再继续移动 。
最后将 拷贝到 从而实现整个 的合并。这样的操作必然能保证 是有序的。
如果还是不懂,参见下面的例子:
初始时 指向 ; 指向 。
因为 ,不产生贡献,所以 , 指向 ;
,所以产生贡献,, 指向 ;
,不产生贡献,, 指向 ;
,产生贡献,, 指向 ;
,不产生贡献,, 指向 ;
如此类推,最终易知 序列有序。
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=5e5+5;
int n,a[N],b[N],ans;
inline 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,now=l;//i,j表示左右指针,now表示b数组的指针
while(i<=mid&&j<=r){
if(a[i]<=a[j])b[now++]=a[i++];//不产生贡献
else{//产生贡献
b[now++]=a[j++];
ans+=mid-i+1;//计算贡献
}
}
while(i<=mid)b[now++]=a[i++];
while(j<=r)b[now++]=a[j++];
for(int i=l;i<=r;i++)a[i]=b[i];
return;
}
signed main(){
scanf("%lld",&n);
for(int i=1;i<=n;i++)scanf("%lld",&a[i]);
cdq(1,n);
printf("%lld",ans);
return 0;
}
通过一道例题来引入 cdq 分治的具体实现。
有 个元素,第 个元素有 三个属性,设 表示满足 且 且 且 的 的数量。
对于 ,求 的数量。
考虑将第一维 通过 sort 解决。。
那么还剩下 两维。
对于当前区间 ,我们通过递归算出了 和 的答案,考虑求 。
那么本质上就是算出有多少对 满足 。
接下来是两种思路。
- 用 sort 解决第二维 。
考虑对于 以 为第二关键字再 sort 一遍。
那么第二维 也解决了。
考虑依次枚举 ,对于每个 的点插入树状数组。
那么查询这个数据结构里有多少个点的 值是小于 的,我们就对于这个点 求出了有多少个 可以匹配了。
具体操作就是每插入一个 时,将 位上 ,那么查询有多少点 相当于是查询一个前缀和。
注意我们已经 sort 好了 这一维,所以在枚举 时考虑线性插入 即可。
总时间复杂度 。
#include<bits/stdc++.h>
using namespace std;
const int N=2e5+6;
int n,m,k,tr[N],ans[N];
struct node{
int x,y,z,cnt,p;
}a[N],f[N];
inline bool cmp1(node a,node b){
if(a.x!=b.x)return a.x<b.x;
if(a.y!=b.y)return a.y<b.y;
return a.z<b.z;
}
inline bool cmp2(node a,node b){
if(a.y!=b.y)return a.y<b.y;
return a.z<b.z;
}
//树状数组模板
inline int lowbit(int x){
return x&(-x);
}
inline void update(int x,int val){
while(x<=k){
tr[x]+=val;
x+=lowbit(x);
}
}
inline int query(int x){
int res=0;
while(x){
res+=tr[x];
x-=lowbit(x);
}
return res;
}
inline void cdq(int l,int r){
if(l==r)return;
int mid=(l+r)>>1;
cdq(l,mid);//递归处理 [l,mid]
cdq(mid+1,r);//递归处理 [mid+1,r]
sort(f+l,f+mid+1,cmp2);
sort(f+mid+1,f+r+1,cmp2);
//对第二关键字排序
int now=l;//双指针
for(int i=mid+1;i<=r;i++){
while(f[now].y<=f[i].y&&now<=mid){
update(f[now].z,f[now].cnt);//插入树状数组
now++;
}
f[i].p+=query(f[i].z);
}
for(int i=l;i<now;i++)update(f[i].z,-f[i].cnt);//清空树状数组
return;
}
signed main(){
scanf("%d%d",&n,&k);
for(int i=1;i<=n;i++)scanf("%d%d%d",&a[i].x,&a[i].y,&a[i].z);
sort(a+1,a+n+1,cmp1);//对第一维关键字排序
int now=0;
for(int i=1;i<=n;i++){
now++;
if(a[i].x!=a[i+1].x||a[i].y!=a[i+1].y||a[i].z!=a[i+1].z){
m++;
f[m].x=a[i].x;
f[m].y=a[i].y;
f[m].z=a[i].z;
f[m].cnt=now;
now=0;
}
}
cdq(1,m);
for(int i=1;i<=m;i++)ans[f[i].p+f[i].cnt]+=f[i].cnt;
for(int i=1;i<=n;i++)printf("%d\n",ans[i]);
return 0;
}
- 归并排序思想解决第二维 。
这种比上一种常数小很多。
考虑直接在统计 的贡献时解决 。
比如当前是 而通过递归,两个区间的第二维 都已经有序。
考虑维护一个数组和指针 ,当遇到 时,考虑将 加入数组后再在后面加入 。然后更新 为 。
举个例子:比如上图,假设我们递归到 。而通过递归显然 的第二维 是有序的。
考虑如何将两者合并。
首先比较 。 比较小,继续比较。
,则将 加入维护的数组,然后加入 。此时指针指向 。
然后同理,,继续比较。
,则将 加入数组,指针指向 。
,继续将 加入数组。指针不变。
,继续比较。
,将 加入。
,将 加入。
最后加入剩余的 。
对于 的处理同第一种方法。
这样可以做到省略了 sort 部分。但由于树状数组查询和修改还带有一个 ,所以时间复杂度仍为 。但是显然常数小很多。
#include<bits/stdc++.h>
using namespace std;
const int N=2e5+6;
int n,k,tr[N],sz[N],cnt,f[N],ans[N];
struct node{
int x,y,z,id;
}a[N],b[N];//a为输入,b即上文提到的用来维护的数组
inline bool cmp(node a,node b){//以第一关键字排序
if(a.x!=b.x)return a.x<b.x;
else if(a.y!=b.y)return a.y<b.y;
else return a.z<b.z;
}
//BIT树状数组板子
inline int lowbit(int x){
return x&(-x);
}
inline void update(int x,int val){
while(x<=k){
tr[x]+=val;
x+=lowbit(x);
}
}
inline int query(int x){
int res=0;
while(x){
res+=tr[x];
x-=lowbit(x);
}
return res;
}
inline 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,now=l;//i,j左右指针
while(i<=mid&&j<=r){//归并+统计答案
if(a[i].y<=a[j].y){
update(a[i].z,sz[a[i].id]);
b[now]=a[i];//将a[i]加入
i++;
now++;
}
else{
ans[a[j].id]+=query(a[j].z);
b[now]=a[j];//将a[j]加入
j++;
now++;
}
//注意因为在子区间中使用了归并,所以两个子区间中第二维肯定是升序的
}
while(j<=r){//将剩下的归并排序完
ans[a[j].id]+=query(a[j].z);
b[now]=a[j];
now++;
j++;
}
for(int I=l;I<i;I++)update(a[I].z,-sz[a[I].id]);//清除树状数组
while(i<=mid){//将剩下的归并排序完
b[now]=a[i];
now++;
i++;
}
for(int i=l;i<=r;i++)a[i]=b[i];//更新原数组a
return;
}
int main(){
scanf("%d%d",&n,&k);
for(int i=1;i<=n;i++)scanf("%d%d%d",&a[i].x,&a[i].y,&a[i].z);
sort(a+1,a+n+1,cmp);//对第一关键字排序
for(int i=1;i<=n;i++){//去重
int j=i-1;
if(a[i].x!=a[j].x||a[i].y!=a[j].y||a[i].z!=a[j].z)b[++cnt]=a[i];
sz[cnt]++;
}
for(int i=1;i<=cnt;i++){
a[i]=b[i];
a[i].id=i;
}
cdq(1,cnt);
for(int i=1;i<=cnt;i++){
int x=a[i].id;
f[ans[x]+sz[x]-1]+=sz[x];
}
for(int i=0;i<n;i++)printf("%d\n",f[i]);
return 0;
}
三. 从三维偏序到四维偏序
给定 个四维点,第 个点为 ,每个点都带有一个点权,第 个点的点权为 。
对于一个合法的路径,满足经过的点的四个坐标全部单调不降;路径的权定义为该路径所经过点的权值和。
求合法路径权的最大值。
。
四维偏序模板题。
这个要求维护四个量,而我们所熟知的 cdq 分治是维护三维偏序的。
那么考虑 cdq 套 cdq 来增加一维。
回顾如何解决用 cdq 分治做三维偏序。
本质就是考虑左边区间对右边区间的影响。
四维偏序同理。
比如我们令 表示三维;那么解决四维偏序就是 对 的影响。这里 表示是当前这一维所讨论的是左区间还是右区间。因为第一维是用 sort
排序好了的,所以讨论的是第二维。
那么如果四维偏序用 表示,我们所讨论的就是 对 的影响。
然后思路同三维偏序,先排序第一维,然后 cdq1
第二维、cdq2
第三、四维。
其中 cdq1,cdq2
是两个 cdq 分治函数。通过第一个函数 cdq1
中嵌套 cdq2
来解决多的一维。
如果理解以上就很简单了,其余的和普通三维偏序没有什么不同。
注意这题是最长上升序列,要在递归 后先计算 对 的贡献,再递归 。
时间复杂度 。
如果是 维偏序,那么朴素的 cdq 分治就是 的。当然这种复杂度渐进意义下优秀,但当 较小又 时,显然 cdq 分治就不如暴力了。
当然这时候用分块对吧。时间复杂度 。
//O2
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int N=5e5+5;
const ll inf=0x7f7f7f7f;
int n,m,lsh[N],pos[N],pos2[N],cnt;
ll ans=-inf;
struct node{
int x,y,z,w,id;
bool flag;
ll p,val;
bool operator ==(const node rhs)const{
return x==rhs.x&&y==rhs.y&&z==rhs.z&&w==rhs.w;
}
}a[N],b[N];
inline bool cmp1(node a,node b){
if(a.x!=b.x) return a.x<b.x;
if(a.y!=b.y) return a.y<b.y;
if(a.z!=b.z) return a.z<b.z;
return a.w<b.w;
}
inline bool cmp2(node a,node b){
if(a.y!=b.y) return a.y<b.y;
if(a.z!=b.z) return a.z<b.z;
if(a.w!=b.w) return a.w<b.w;
return a.x<b.x;
}
inline bool cmp3(node a,node b){
if(a.z!=b.z) return a.z<b.z;
if(a.w!=b.w) return a.w<b.w;
if(a.x!=b.x) return a.x<b.x;
return a.y<b.y;
}
//注意排序要彻底
struct BIT{//树状数组
ll tr[N];
inline int lowbit(int x){
return x&(-x);
}
inline void update(int x,ll val){
while(x<=n){
tr[x]=max(tr[x],val);
x+=lowbit(x);
}
return;
}
inline ll query(int x){
ll res=-inf;
while(x){
res=max(res,tr[x]);
x-=lowbit(x);
}
return res;
}
inline void clear(int x){
while(x&&x<=n){
tr[x]=0;
x+=lowbit(x);
}
return;
}
}tr;
inline void cdq2(int l,int r){//第二层的cdq
if(l==r)return;
int mid=(l+r)>>1;
cdq2(l,mid);
sort(a+l,a+mid+1,cmp3);
sort(a+mid+1,a+r+1,cmp3);
int i=l,j=mid+1;
while(j<=r){
while(i<=mid&&a[i].z<=a[j].z){
if(!a[i].flag)tr.update(a[i].w,a[i].p);
i++;
}
if(a[j].flag)a[j].p=max(a[j].p,tr.query(a[j].w)+a[j].val);
j++;
}
int now=i;
for(int i=l;i<now;i++)if(!a[i].flag)tr.clear(a[i].w);
for(int i=l;i<=r;i++)b[pos2[a[i].id]]=a[i];
for(int i=l;i<=r;i++)a[i]=b[i];
cdq2(mid+1,r);
return;
}
inline void cdq1(int l,int r){//第一层的cdq
if(l==r)return;
int mid=(l+r)>>1;
cdq1(l,mid);
for(int i=l;i<=mid;i++)a[i].flag=false;
for(int i=mid+1;i<=r;i++)a[i].flag=true;
sort(a+l,a+r+1,cmp2);
for(int i=l;i<=r;i++)pos2[a[i].id]=i;
cdq2(l,r);
for(int i=l;i<=r;i++)b[pos[a[i].id]]=a[i];
for(int i=l;i<=r;i++)a[i]=b[i];
cdq1(mid+1,r);
return;
}
int main(){
scanf("%d",&n);
for(int i=1;i<=n;++i){
scanf("%d%d%d%d%lld",&a[i].x,&a[i].y,&a[i].z,&a[i].w,&a[i].val);
lsh[i]=a[i].w;
}
sort(lsh+1,lsh+n+1);
m=unique(lsh+1,lsh+n+1)-lsh-1;//离散化
for(int i=1;i<=n;i++)a[i].w=lower_bound(lsh+1,lsh+m+1,a[i].w)-lsh;
sort(a+1,a+n+1,cmp1);
for(int i=1;i<=n;i++){
if(a[i]==a[i+1])a[cnt].val+=max(0ll,a[i].val);
else a[++cnt]=a[i];
}
for(int i=1;i<=n;i++){
a[i].p=a[i].val;
a[i].id=i;
pos[i]=i;
}
cdq1(1,n);
for(int i=1;i<=n;i++)ans=max(ans,a[i].p);
printf("%lld\n",ans);
return 0;
}
四. cdq 将动态问题转化为静态问题
本质上只是考虑将询问的时间作为一维加入 cdq 分治。
将询问离线,要求所有操作会按照时间自然地排成一个序列,且每一个修改均与之后的询问操作有关。
考虑将 处理修改操作, 处理询问。
注意对于求 对 的影响时的操作顺序。
如果修改之间独立(如加减)则无需严格按照时间操作执行;若不独立(如赋值),要先处理左区间对右区间的贡献后再递归右区间。
这个性质在下文的 cdq 优化 dp 中同样将用到。
-
矩形加与和
维护一个二维平面 支持在一个矩形区域内加一个数字,每次询问一个矩形区域的和。
显然对于静态版本,通过扫描线处理是一个大家都会的做法。
增加了动态的修改后考虑使用 cdq 分治。由于左区间处理修改,右区间处理查询,发现每次计算左区间对右区间的影响时当前的修改已处理完,故转化为一个静态问题,再使用扫描线容易。
五. cdq 分治优化1D动态规划
1D 就是一维 dp,转移 的一类dp。
有时可以通过 cdq 分治优化。
但是这里 cdq 分治的顺序应该改变:
-
递归
-
处理左区间对于右区间的影响,并对于右区间或者答案进行更改与修正。
-
递归
考虑第二步的做法,和普通的 cdq 分治一样的做法:双指针和树状数组查询前缀和一类的操作。
与普通的 cdq 分治操作顺序不同的是,这种情况要先处理左区间对右区间的影响再递归 。
考虑为什么要这样。
普通 cdq 的写法是,当递归到 时,会计算它内部的相互贡献,然后通过 计算对 的贡献。
但是有可能出现计算左边部分对右边部分的贡献的结果会影响到右边部分内部的情况,所以不能先计算左右区间内部贡献。正如我们之前所遇到过的,如求最长上升子序列,前后部分的影响并不独立。
因为这种情况需要保证当递归 时, 的值必须全部计算好。所以在递归 时要保证左边区间不再对右边做出贡献。
有 个导弹,每个导弹有三个参数 , 和 。你需要求出一个最长的序列 ,满足对于所有的 均有 。输出最长的序列的长度。由于可能有多种最长的序列的方案,每次随机选一种,你需要求出对于每个导弹,其成为最长序列中的一项的概率。
显然比起原题导弹拦截多了一维。
考虑 dp 转移,记录最长上升子序列的长度与方案数容易做到 。
那么这样可以使用 cdq 分治处理,然后注意分支顺序是先处理左区间对右区间的影响再计算右区间。
实现时注意归并时不需要处理未遍历完的左区间剩余的数,因为此时对右区间的查询无法造成影响。否则会超时。
#include<bits/stdc++.h>
using namespace std;
const int N=5e4+5;
int n,tr[N],f[N],g[N],ans;
double val[N],F[N],G[N],base;
struct node{
int x,y,id,k;
double v;
}a[N];
inline bool cmp1(node a,node b){
if(a.x!=b.x)return a.x>b.x;
if(a.y!=b.y)return a.y>b.y;
return a.id<b.id;
}
inline bool cmp2(node a,node b){
if(a.x!=b.x)return a.x<b.x;
if(a.y!=b.y)return a.y<b.y;
return a.id<b.id;
}
inline bool cmp3(node a,node b){
if(a.y!=b.y)return a.y>b.y;
return a.id<b.id;
}
inline bool cmp4(node a,node b){
if(a.y!=b.y)return a.y<b.y;
return a.id<b.id;
}
inline int lowbit(int x){
return x&-x;
}
inline void update(int x,int k,double v){
while(x<=n){
if(tr[x]<k)tr[x]=k,val[x]=v;
else if(tr[x]==k)val[x]+=v;
x+=lowbit(x);
}
return;
}
inline int find(int x){
int res=0;
while(x){
res=max(res,tr[x]);
x-=lowbit(x);
}
return res;
}
inline double query(int x,int k){
double res=0;
while(x){
if(tr[x]==k)res+=val[x];
x-=lowbit(x);
}
return res;
}
inline void clear(int x){
while(x<=n){
tr[x]=0,val[x]=0.0;
x+=lowbit(x);
}
return;
}
inline bool check(int i,int j,int o){
if(o==1)return a[i].y>=a[j].y;
else return a[i].y<=a[j].y;
}
inline void cdq2(int l,int r,int o){
int mid=(l+r)>>1,i=l,j=mid+1;
while(i<=mid&&j<=r){
if(check(i,j,o))update(a[i].id,a[i].k,a[i].v),i++;
else{
int ps=find(a[j].id)+1;
if(a[j].k<ps)a[j].k=ps,a[j].v=query(a[j].id,ps-1);
else if(a[j].k==ps)a[j].v+=query(a[j].id,ps-1);
j++;
}
}
while(j<=r){
int ps=find(a[j].id)+1;
if(a[j].k<ps)a[j].k=ps,a[j].v=query(a[j].id,ps-1);
else if(a[j].k==ps)a[j].v+=query(a[j].id,ps-1);
j++;
}
for(int I=l;I<i;I++)clear(a[I].id);
return;
}
inline void cdq1(int l,int r,int o){
if(l==r)return;
int mid=(l+r)>>1;
cdq1(l,mid,o);
if(o==1)sort(a+l,a+mid+1,cmp3),sort(a+mid+1,a+r+1,cmp3);
else sort(a+l,a+mid+1,cmp4),sort(a+mid+1,a+r+1,cmp4);
cdq2(l,r,o);
if(o==1)sort(a+mid+1,a+r+1,cmp1);
else sort(a+mid+1,a+r+1,cmp2);
cdq1(mid+1,r,o);
return;
}
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++){
scanf("%d%d",&a[i].x,&a[i].y),a[i].id=i;
a[i].k=1,a[i].v=1.0;
}
sort(a+1,a+n+1,cmp1);
cdq1(1,n,1);
for(int i=1;i<=n;i++){
f[a[i].id]=a[i].k,F[a[i].id]=a[i].v;
ans=max(ans,f[a[i].id]);
}
printf("%d\n",ans);
for(int i=1;i<=n;i++){
a[i].id=n-a[i].id+1;
a[i].k=1,a[i].v=1.0;
}
sort(a+1,a+n+1,cmp2);
cdq1(1,n,2);
for(int i=1;i<=n;i++){
int j=n-a[i].id+1;
g[j]=a[i].k,G[j]=a[i].v;
if(g[j]==ans)base+=G[j];
}
for(int i=1;i<=n;i++){
if(f[i]+g[i]==ans+1)printf("%.5lf ",F[i]*G[i]/base);
else printf("0.00000 ");
}
return 0;
}
六. 习题
二维偏序:
常规三维偏序:
杂类三维偏序
四维偏序
cdq 分治优化 dp
cdq 分治将动态问题转化为静态问题
本文作者:trsins
本文链接:https://www.cnblogs.com/trsins/p/17970740
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
2022-01-17 【做题记录】Ynoi2018 天降之物
2022-01-17 【学术】连分数
2022-01-17 【做题记录】Ynoi2015 盼君勿忘
2022-01-17 【做题记录】BJOI2016 水晶
2022-01-17 【做题记录】P4965 薇尔莉特的打字机
2022-01-17 【做题记录】POI2011 Lightning Conductor
2022-01-17 【做题记录】CF961G Partitions