前言

一直感觉 CDQ 分治是一个很高级的算法,但其实 CDQ 分治的思想早就接触过了。其实,也不是很困难嘛。

此类知识点大纲中并未涉及,所以【8】是我自己的估计,后带星号表示估计,仅供参考。

CDQ 分治

我没有找到 CDQ 分治的严格定义,所以一下只是我的理解,并不一定非常准确。

先从一般分治算法入手。分治的思想是把问题划分为若干个独立的子问题,通过递归来求解每个子问题。

但如果子问题之间相互有贡献,那我们还需要处理子问题之间的贡献,这种分治就叫做 CDQ 分治。

CDQ 分治最常见的情况就是将问题划分为两个子问题,然后先递归求解第一个问题,再处理第一个问题对第二个问题的贡献,最后再递归第二个问题。在序列上,我们一般通过二分取中间点的方式来划分子问题。不计其余复杂度,假设每个子问题时间复杂度为 \(O(\text{区间长度})\),总时间复杂度为 \(O(n\log n)\)。

CDQ 分治有几个经典应用。一是处理三维偏序问题(其实也可以扩展到更高维),二是将数据结构离线下来通过 CDQ 处理贡献(其实就是转化为三维偏序问题),三是利用 CDQ 分治来优化 DP(其实依旧是三位偏序问题)。分别对应三道例题。

例题

例题 \(1\) :

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

我们使用使用 CDQ 分治,每次将区间折半,递归子问题,之后考虑左子区间对右子区间的贡献。

对初始序列按 \(a_i\) 排序。这样,左子区间的所有数的 \(a_i\) 一定比右子区间的所有数的 \(a_i\) 要小,消掉一维。我们把左、右子区间的所有数分别按照 \(b_i\) 排序,这样,我们就可以遍历右子区间的所有元素,通过双指针找出左子区间中 \(b_i\) 小于当前右子区间的元素 \(b_i\) 的元素,又消掉了一维。最后,我们通过树状数组以 \(c_i\) 为下标维护满足 \(b_i\) 范围内 \(c_i\) 小于等于某个值的元素的数量,就可以通过右子区间的元素的 \(c_i\) 统计题目要求的 \(f(i)\) 了。

注意如果有重复元素的话,右边元素可能会对左边元素有贡献,这是 CDQ 无法处理的,所以我们还需要去重。

时间复杂度 \(O(n\log^2n)\)。

#include <bits/stdc++.h>
using namespace std;
struct node
{
int a,b,c,v,ans;
}a[300000];
int n,m,d,c[300000],ans[300000],sum=0;
bool cmp1(struct node a,struct node b)
{
return (a.a==b.a)?((a.b==b.b)?(a.c<b.c):(a.b<b.b)):(a.a<b.a);
} bool cmp2(struct node a,struct node b)
{
return (a.b==b.b)?(a.c<b.c):(a.b<b.b);
} int lowbit(int x)
{
return x&(-x);
} void add(int x,int k)
{
while(x<=d)c[x]+=k,x+=lowbit(x);
} int getsum(int x)
{
int ans=0;
while(x>0)ans+=c[x],x-=lowbit(x);
return ans;
} void cdq(int l,int r)
{
if(l==r)return;
int mid=(l+r)/2,i=l,j=mid+1;
cdq(l,mid);
cdq(mid+1,r);
sort(a+l,a+mid+1,cmp2);
sort(a+mid+1,a+r+1,cmp2);
for(int j=mid+1;j<=r;j++)
{
while(a[i].b<=a[j].b&&i<=mid)add(a[i].c,a[i].v),i++;
a[j].ans+=getsum(a[j].c);
}
for(int j=l;j<i;j++)add(a[j].c,-a[j].v);
} int main()
{
scanf("%d%d",&n,&d);
for(int i=1;i<=n;i++)
scanf("%d%d%d",&a[i].a,&a[i].b,&a[i].c);
sort(a+1,a+n+1,cmp1);
for(int i=1;i<=n;i++)
{
sum++;
if(a[i].a!=a[i+1].a||a[i].b!=a[i+1].b||a[i].c!=a[i+1].c)
{
a[++m].a=a[i].a;
a[m].b=a[i].b;
a[m].c=a[i].c;
a[m].v=sum;
sum=0;
}
}
cdq(1,m);
for(int i=1;i<=m;i++)ans[a[i].ans+a[i].v-1]+=a[i].v;
for(int i=0;i<n;i++)printf("%d\n",ans[i]);
return 0;
}

例题 \(2\) :

P4169 [Violet] 天使玩偶/SJY摆棋子

首先可以离线,这是我们可以把时间作为一个维度在操作间形成偏序关系,从而转化为三位偏序问题。这也是离线处理这种带修改的问题的经典方式之一。

对于这种带绝对值的问题,我们考虑按绝对值的正负性分类讨论。例如,本题中根据 \(x,y\) 两维的绝对值分类讨论,可以把原平面分成询问点左上,右上,右下,左下四个部分。四个部分做法大同小异,以左上为例。

考虑什么样的插入 \(j\) 会对一个询问 \(i\) 有贡献。首先操作时间必须满足 \(t_j\le t_i\),且因为是左上所以 \(x_j\ge x_i,y_j\ge y_i\)。发现这个偏序关系有三维,考虑 CDQ 分治。

然后考虑贡献是什么。由于位置确定,直接拆掉绝对值有 \(x_j+y_j-x_i-y_i\)。因此,我们想让距离最小,就让 \(x_j+y_j\) 最小,之后减掉 \(x_i+y_i\)。

这样思路就清晰了。操作已经按照时间 \(t_i\) 排好了序,那么我们直接分治。左右分别按 \(x_i\) 排序,双指针求出来 \(j\) 的区间。用单点 \(\text{chkmax}\) 前缀 \(\max\) 维护 \(y_i\) 的偏序关系,插入 \(j\) 时在 \(y_j\) 处 \(\text{chkmax}\;x_j+y_j\)。

记得清空。时间复杂度 \(O(n\log^2n)\)。

#include <bits/stdc++.h>
using namespace std;
struct ask
{
int t,x,y,p;
}q[800000],qp[800000];
int n,m,c[1200000],ans[800000];
bool cmp1(struct ask a,struct ask b)
{
return a.x>b.x;
} bool cmp2(struct ask a,struct ask b)
{
return a.x<b.x;
} int lowbit(int x)
{
return x&(-x);
} void clear(int x)
{
x++;
while(x<=1e6+1)c[x]=1e8,x+=lowbit(x);
} void add(int x,int k)
{
x++;
while(x<=1e6+1)c[x]=min(c[x],k),x+=lowbit(x);
} int query(int x)
{
x++;
int ans=1e8;
while(x>0)ans=min(ans,c[x]),x-=lowbit(x);
return ans;
} void cdq1(int l,int r)
{
if(l==r)return;
int mid=(l+r)>>1;
cdq1(l,mid),cdq1(mid+1,r);
sort(q+l,q+mid+1,cmp1),sort(q+mid+1,q+r+1,cmp1);
int j=l;
for(int i=mid+1;i<=r;i++)
{
while(q[j].x>=q[i].x&&j<=mid)
{
if(q[j].t==1)add(1e6-q[j].y+1,q[j].x+q[j].y);
j++;
}
if(q[i].t==2)ans[q[i].p]=min(ans[q[i].p],query(1e6-q[i].y+1)-q[i].x-q[i].y);
}
for(int i=l;i<=mid;i++)
if(q[i].t==1)clear(1e6-q[i].y+1);
} void cdq2(int l,int r)
{
if(l==r)return;
int mid=(l+r)>>1;
cdq2(l,mid),cdq2(mid+1,r);
sort(q+l,q+mid+1,cmp2),sort(q+mid+1,q+r+1,cmp2);
int j=l;
for(int i=mid+1;i<=r;i++)
{
while(q[j].x<=q[i].x&&j<=mid)
{
if(q[j].t==1)add(1e6-q[j].y+1,q[j].y-q[j].x);
j++;
}
if(q[i].t==2)ans[q[i].p]=min(ans[q[i].p],query(1e6-q[i].y+1)+q[i].x-q[i].y);
}
for(int i=l;i<=mid;i++)
if(q[i].t==1)clear(1e6-q[i].y+1);
} void cdq3(int l,int r)
{
if(l==r)return;
int mid=(l+r)>>1;
cdq3(l,mid),cdq3(mid+1,r);
sort(q+l,q+mid+1,cmp2),sort(q+mid+1,q+r+1,cmp2);
int j=l;
for(int i=mid+1;i<=r;i++)
{
while(q[j].x<=q[i].x&&j<=mid)
{
if(q[j].t==1)add(q[j].y,-q[j].x-q[j].y);
j++;
}
if(q[i].t==2)ans[q[i].p]=min(ans[q[i].p],query(q[i].y)+q[i].x+q[i].y);
}
for(int i=l;i<=mid;i++)
if(q[i].t==1)clear(q[i].y);
} void cdq4(int l,int r)
{
if(l==r)return;
int mid=(l+r)>>1;
cdq4(l,mid),cdq4(mid+1,r);
sort(q+l,q+mid+1,cmp1),sort(q+mid+1,q+r+1,cmp1);
int j=l;
for(int i=mid+1;i<=r;i++)
{
while(q[j].x>=q[i].x&&j<=mid)
{
if(q[j].t==1)add(q[j].y,q[j].x-q[j].y);
j++;
}
if(q[i].t==2)ans[q[i].p]=min(ans[q[i].p],query(q[i].y)-q[i].x+q[i].y);
}
for(int i=l;i<=mid;i++)
if(q[i].t==1)clear(q[i].y);
} int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)scanf("%d%d",&q[i].x,&q[i].y),q[i].t=1;
for(int i=n+1;i<=n+m;i++)
{
scanf("%d",&q[i].t);
if(q[i].t==1)scanf("%d%d",&q[i].x,&q[i].y);
else scanf("%d%d",&q[i].x,&q[i].y),q[i].p=i;
}
for(int i=1;i<=n+m;i++)qp[i]=q[i],ans[i]=1e8;
for(int i=1;i<=1e6+1;i++)c[i]=1e8;
cdq1(1,n+m);
for(int i=1;i<=n+m;i++)q[i]=qp[i];
cdq2(1,n+m);
for(int i=1;i<=n+m;i++)q[i]=qp[i];
cdq3(1,n+m);
for(int i=1;i<=n+m;i++)q[i]=qp[i];
cdq4(1,n+m);
for(int i=n+1;i<=n+m;i++)
if(qp[i].t==2)printf("%d\n",ans[i]);
return 0;
}

例题 \(3\) :

P4093 [HEOI2016/TJOI2016] 序列

本题是 CDQ 优化 DP 的经典应用。

我们先写出朴素 DP。设 \(f[i]\) 表示以 \(i\) 结尾最长满足条件的序列的长度,\(v[i]\) 为位置 \(i\) 初始的值,\(mx[i]\) 为位置 \(i\) 的最大取值,\(mi[i]\) 为位置 \(i\) 的最小取值。不难推出如下转移方程。

\[f[i]=\max\{f[j]\}+1(j\lt i,mx[j]\le v[i],v[j]\le mi[i])
\]

这是一个三位偏序问题,我们考虑在维护 CDQ 分治的同时通过处理贡献转移 DP。具体的,我们按照位置排序,分成左右两个子区间,递归求解。由于左边对右边有影响,所以先递归左边,再处理左边对右边的贡献,最后递归右边。

把左边按照 \(v\) 升序排序,右边按照 \(mi\) 升序排序,这样在枚举右边的位置时左边的可选的位置可以通过双指针求出。然后用树状数组维护以 \(mx\) 为下标,\(f\) 为值的单点 \(\text{chkmax}\) 前缀 \(\max\),对于右边的 \(i\) 查询小于等于 \(v[i]\) 的前缀 \(\max\) 转移 DP。

时间复杂度 \(O(n\log^2n)\)。

#include <bits/stdc++.h>
using namespace std;
struct val
{
int v,mx,mi,f,p;
}a[200000];
int n,m,x,y,c[200000],ans=0;
int lowbit(int x)
{
return x&(-x);
} void clear(int x)
{
x++;
while(x<=1e5+1)c[x]=0,x+=lowbit(x);
} void add(int x,int k)
{
x++;
while(x<=1e5+1)c[x]=max(c[x],k),x+=lowbit(x);
} int query(int x)
{
x++;
int ans=0;
while(x>0)ans=max(ans,c[x]),x-=lowbit(x);
return ans;
} bool cmp1(struct val a,struct val b)
{
return a.v<b.v;
} bool cmp2(struct val a,struct val b)
{
return a.mi<b.mi;
} bool cmp3(struct val a,struct val b)
{
return a.p<b.p;
} void cdq(int l,int r)
{
if(l==r)return;
int mid=(l+r)>>1;
cdq(l,mid),sort(a+l,a+mid+1,cmp1),sort(a+mid+1,a+r+1,cmp2);
int j=l;
for(int i=mid+1;i<=r;i++)
{
while(a[j].v<=a[i].mi&&j<=mid)add(a[j].mx,a[j].f),j++;
a[i].f=max(a[i].f,query(a[i].v)+1);
}
for(int i=l;i<=mid;i++)clear(a[i].mx);
sort(a+mid+1,a+r+1,cmp3);
cdq(mid+1,r);
} int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)scanf("%d",&a[i].v),a[i].mx=a[i].mi=a[i].v,a[i].p=i;
for(int i=1;i<=m;i++)scanf("%d%d",&x,&y),a[x].mx=max(a[x].mx,y),a[x].mi=min(a[x].mi,y);
cdq(0,n);
for(int i=1;i<=n;i++)ans=max(ans,a[i].f);
printf("%d\n",ans);
return 0;
}

后记

CDQ 分治似乎大多都可以被高维数据结构替代,但 CDQ 码量小,常数小,还是很有优势的。

三千 铁衣披霜 万籁绝响 举目是残阳

回首 剑拔弩张 箭已经在弦上

耳畔 江海有声 山河无量 呼万寿无疆

却无人共看 这人间多荒唐

【8*】CDQ分治学习笔记的更多相关文章

  1. 初学cdq分治学习笔记(可能有第二次的学习笔记)

    前言骚话 本人蒟蒻,一开始看到模板题就非常的懵逼,链接,学到后面就越来越清楚了. 吐槽,cdq,超短裙分治....(尴尬) 正片开始 思想 和普通的分治,还是分而治之,但是有一点不一样的是一般的分治在 ...

  2. CDQ分治学习笔记

    数据结构中的一块内容:$CDQ$分治算法. $CDQ$显然是一个人的名字,陈丹琪(NOI2008金牌女选手) 这种离线分治算法被算法界称为"cdq分治" 我们知道,一个动态的问题一 ...

  3. [摸鱼]cdq分治 && 学习笔记

    待我玩会游戏整理下思绪(分明是想摸鱼 cdq分治是一种用于降维和处理对不同子区间有贡献的离线分治算法 对于常见的操作查询题目而言,时间总是有序的,而cdq分治则是耗费\(O(logq)\)的代价使动态 ...

  4. CDQ分治学习笔记(三维偏序题解)

    首先肯定是要膜拜CDQ大佬的. 题目背景 这是一道模板题 可以使用bitset,CDQ分治,K-DTree等方式解决. 题目描述 有 nn 个元素,第 ii 个元素有 a_iai​.b_ibi​.c_ ...

  5. cdq 分治学习笔记

    CDQ 分治是一种很玄学的东西. Part 0 引子 相信大家都会归并排序,又都知道归并排序求逆序对怎么求的 如果连归并求逆序对都不会那你为什么要学 cdq 分治阿喂 void merge_sort( ...

  6. 三维偏序[cdq分治学习笔记]

    三维偏序 就是让第一维有序 然后归并+树状数组求两维 cdq+cdq不会 告辞 #include <bits/stdc++.h> // #define int long long #def ...

  7. CDQ分治学习思考

    先挂上个大佬讲解,sunyutian1998学长给我推荐的mlystdcall大佬的[教程]简易CDQ分治教程&学习笔记 还有个B站小姐姐讲解的概念https://www.bilibili.c ...

  8. cdq分治学习

    看了stdcall大佬的博客 传送门: http://www.cnblogs.com/mlystdcall/p/6219421.html 感觉cdq分治似乎很多时候都要用到归并的思想

  9. [Updating]点分治学习笔记

    Upd \(2020/2/15\),又补了一题 LuoguP2664 树上游戏 \(2020/2/14\),补了一道例题 LuoguP3085 [USACO13OPEN]阴和阳Yin and Yang ...

  10. 点分治&&动态点分治学习笔记

    突然发现网上关于点分和动态点分的教程好像很少……蒟蒻开篇blog记录一下吧……因为这是个大傻逼,可能有很多地方写错,欢迎在下面提出 参考文献:https://www.cnblogs.com/LadyL ...

随机推荐

  1. 🔥Elasticsearch(ES)(版本7.x)数据更新后刷新策略RefreshPolicy

    简介 在Elasticsearch 7.x版本中,当更新数据时(例如索引.更新或删除文档),这些更改并不会立即对搜索可见.为了让这些更改能够被搜索到,需要了解和选择合适的刷新策略(Refresh Po ...

  2. Spring基于XML的事务管理器DataSourceTransactionManager

    Spring基于XML的事务管理器DataSourceTransactionManager 源码 代码测试 pom.xml <?xml version="1.0" encod ...

  3. K8s新手系列之namespace

    概述 官方文档地址:https://kubernetes.io/zh-cn/docs/tasks/administer-cluster/namespaces/ namespace是K8s系统中的一种非 ...

  4. 【代码】Android|获取压力传感器、屏幕压感数据(大气压、原生和Processing)

    首先需要分清自己需要的是大气压还是触摸压力,如果是大气压那么就是TYPE_PRESSURE,可以参考https://source.android.google.cn/docs/core/interac ...

  5. Mysql 实现 rank 和 != 问题

    我一直相信, 人是能预测未来的, 这应该是前几年看弗洛伊德, 荣格的一些心理学书, 给我的一些感受, 有个片段是关于做梦的, 一个人梦见子弹穿过他自己的头颅, 结果不久, 他就去世了. 这个片段当时给 ...

  6. Python中strftime()与strptime()的行为与datetime的时间格式码

    前言 datetime在python中的作用不可小视,它可以与string进行相互转化,比如 import datetime # 将输出当前时间的'日/月/年' datetime.datetime.n ...

  7. 1-1, 一个简单的mysql 安装教程,基于mysql 5.7解压版本.

    下载mysql. 略,去官网 1. 配置my.cnf. 把mysql提供的配置模板 copy到/etc/my.cnf (先读取/etc/my.cnf,再去读/etc/mysql/my.cnf,第三个读 ...

  8. AI赋能金融风控:基于机器学习的智能欺诈检测系统实战教程

    引言 在数字化转型浪潮中,金融欺诈手段呈现智能化.隐蔽化趋势.传统规则引擎已难以应对复杂多变的欺诈模式,而机器学习技术通过自动学习数据特征,正在重塑金融风控体系.本文将基于Python生态,以信用卡欺 ...

  9. JAVA基础-跳出循环的4种方式

    摘要:介绍4种跳出循环的方式,尤其是其中的break 标签,有时候真的会成为工作中的秘密武器.   在实际编程中,有时需要在条件语句匹配的时候跳出循环.在 Java 语言里,由关键词 break 和 ...

  10. 把多个文件打包压缩成tar.gz文件并解压的Java实现

    压缩文件   在Java中,可以 使用GZIPOutputStream创建gzip(gz)压缩文件,它在commons-compress下面,可以通过如下的maven坐标引入: <depende ...