【8*】CDQ分治学习笔记
前言
一直感觉 CDQ 分治是一个很高级的算法,但其实 CDQ 分治的思想早就接触过了。其实,也不是很困难嘛。
此类知识点大纲中并未涉及,所以【8】是我自己的估计,后带星号表示估计,仅供参考。
CDQ 分治
我没有找到 CDQ 分治的严格定义,所以一下只是我的理解,并不一定非常准确。
先从一般分治算法入手。分治的思想是把问题划分为若干个独立的子问题,通过递归来求解每个子问题。
但如果子问题之间相互有贡献,那我们还需要处理子问题之间的贡献,这种分治就叫做 CDQ 分治。
CDQ 分治最常见的情况就是将问题划分为两个子问题,然后先递归求解第一个问题,再处理第一个问题对第二个问题的贡献,最后再递归第二个问题。在序列上,我们一般通过二分取中间点的方式来划分子问题。不计其余复杂度,假设每个子问题时间复杂度为 \(O(\text{区间长度})\),总时间复杂度为 \(O(n\log n)\)。
CDQ 分治有几个经典应用。一是处理三维偏序问题(其实也可以扩展到更高维),二是将数据结构离线下来通过 CDQ 处理贡献(其实就是转化为三维偏序问题),三是利用 CDQ 分治来优化 DP(其实依旧是三位偏序问题)。分别对应三道例题。
例题
例题 \(1\) :
我们使用使用 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\) :
首先可以离线,这是我们可以把时间作为一个维度在操作间形成偏序关系,从而转化为三位偏序问题。这也是离线处理这种带修改的问题的经典方式之一。
对于这种带绝对值的问题,我们考虑按绝对值的正负性分类讨论。例如,本题中根据 \(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\) :
本题是 CDQ 优化 DP 的经典应用。
我们先写出朴素 DP。设 \(f[i]\) 表示以 \(i\) 结尾最长满足条件的序列的长度,\(v[i]\) 为位置 \(i\) 初始的值,\(mx[i]\) 为位置 \(i\) 的最大取值,\(mi[i]\) 为位置 \(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分治学习笔记的更多相关文章
- 初学cdq分治学习笔记(可能有第二次的学习笔记)
前言骚话 本人蒟蒻,一开始看到模板题就非常的懵逼,链接,学到后面就越来越清楚了. 吐槽,cdq,超短裙分治....(尴尬) 正片开始 思想 和普通的分治,还是分而治之,但是有一点不一样的是一般的分治在 ...
- CDQ分治学习笔记
数据结构中的一块内容:$CDQ$分治算法. $CDQ$显然是一个人的名字,陈丹琪(NOI2008金牌女选手) 这种离线分治算法被算法界称为"cdq分治" 我们知道,一个动态的问题一 ...
- [摸鱼]cdq分治 && 学习笔记
待我玩会游戏整理下思绪(分明是想摸鱼 cdq分治是一种用于降维和处理对不同子区间有贡献的离线分治算法 对于常见的操作查询题目而言,时间总是有序的,而cdq分治则是耗费\(O(logq)\)的代价使动态 ...
- CDQ分治学习笔记(三维偏序题解)
首先肯定是要膜拜CDQ大佬的. 题目背景 这是一道模板题 可以使用bitset,CDQ分治,K-DTree等方式解决. 题目描述 有 nn 个元素,第 ii 个元素有 a_iai.b_ibi.c_ ...
- cdq 分治学习笔记
CDQ 分治是一种很玄学的东西. Part 0 引子 相信大家都会归并排序,又都知道归并排序求逆序对怎么求的 如果连归并求逆序对都不会那你为什么要学 cdq 分治阿喂 void merge_sort( ...
- 三维偏序[cdq分治学习笔记]
三维偏序 就是让第一维有序 然后归并+树状数组求两维 cdq+cdq不会 告辞 #include <bits/stdc++.h> // #define int long long #def ...
- CDQ分治学习思考
先挂上个大佬讲解,sunyutian1998学长给我推荐的mlystdcall大佬的[教程]简易CDQ分治教程&学习笔记 还有个B站小姐姐讲解的概念https://www.bilibili.c ...
- cdq分治学习
看了stdcall大佬的博客 传送门: http://www.cnblogs.com/mlystdcall/p/6219421.html 感觉cdq分治似乎很多时候都要用到归并的思想
- [Updating]点分治学习笔记
Upd \(2020/2/15\),又补了一题 LuoguP2664 树上游戏 \(2020/2/14\),补了一道例题 LuoguP3085 [USACO13OPEN]阴和阳Yin and Yang ...
- 点分治&&动态点分治学习笔记
突然发现网上关于点分和动态点分的教程好像很少……蒟蒻开篇blog记录一下吧……因为这是个大傻逼,可能有很多地方写错,欢迎在下面提出 参考文献:https://www.cnblogs.com/LadyL ...
随机推荐
- 🎯Vercel-从零到上线的云端部署神器
简介 Vercel是一个专注于前端和全栈应用部署的云端平台,由Zeit公司开发.它以零配置部署.全球CDN加速和对主流框架的深度支持为核心优势,成为开发者快速上线项目的首选工具.无论是个人博客.企业官 ...
- C#关键字:in、out、ref、in T、out T、[In]、[Out]这些你都知道多少?
in.out 和 ref 关键字 首先我们来说in.out 和 ref ,在 C# 中,in.out 和 ref 是用于方法参数的引用传递.在引用传递过程中,形参和实参都是指向相同的引用地址. 名称 ...
- 基于OpenCV与Tesseract的文档扫描增强器实战教程(附完整代码)
引言:文档数字化的智能解决方案 在移动办公时代,手机拍摄文档已成为常态,但随之带来的图像畸变.光照不均.文字倾斜等问题严重影响OCR识别效果.本文将通过OpenCV和Tesseract构建一款具备实时 ...
- layui动态渲染select表单、初始化默认值、change事件监听等
layui动态渲染,初始化默认值 var html = ''; html += '<select name="" lay-search lay-verify="re ...
- Spring JdbcTemplate操作数据库
Spring JdbcTemplate操作数据库 源码 代码测试 pom.xml <?xml version="1.0" encoding="UTF-8" ...
- Java编程--单例(Singleton)设计模式
单例设计模式 一个类只有一个实例,根据创建的时机又分为懒汉式和饿汉式,它们的区别主要体现在实例的创建时机和线程安全性上. 饿汉式(Eager Initialization): 特点: 在类加载时就创建 ...
- 一些 DP 思维题
最单纯的思维题就是想出来思路就会做,几乎没有实现难度的题.这种题 CF 与 Atcoder 比较多,这里集中记录一下. 对于 DP 而言,思维题只需要想出转移方程即可. CF1174E Ehab an ...
- pod数据持久化-pv与pvc资源及动态存储StorageClass
一.pc与pvc的概念 在传统的存储卷挂载,比如说nfs,它虽然能够实现我们大多数的生产场景,但是,耦合性比较高: 举例:假设,我们要将集群从"阿里云"迁移到我们私有云服务器上,并 ...
- 国产化-消息队列RocketMq(替代kafka)-单节点安装
RocketMQ 是一款由阿里巴巴开源的分布式消息中间件,具有高可靠.高性能.高可扩展性等特点,在众多企业级应用中得到了广泛的应用.以下是对 RocketMQ 的详细介绍: 国内三大IT巨头阿里. ...
- springAI实现一个MCP-Server
mcp Model Context Protocol(MCP)模型上下文协议(如下图所示)是 Anthropic 发布的一种标准化协议,使得 Agent 智能体应用可以更快捷地与下游异构的数据或者工具 ...