CDQ分治【分治(真得头疼)
前言:
说实话我对于这种没有固定板子,变化多端的算法实在是非常头疼的
但是不学不行,这也是一种很重要的思伟方式
所以趁着这几天的心情比较好(快要放大周),赶紧学一波~
鸣谢:tham,stdcall
CDQ分治,传说中是一个♀神犇创造的算法
在了解这种算法之前,我们有必要了解一下一种基本的思想:分治
知识储备:分治
- 分治介绍
分而治之,将原问题不断划分成若干个子问题,直到子问题规模小到足以直接解决
子问题间互相独立且原问题形式相同,递归求解这些子问题,然后将各子问题的解合并得到原问题的解 - 一般步骤
- 划分 Divide
将原问题划分成若干子问题,子问题间互相独立且与原问题形式相同 - 解决 Conquer
递归解决子问题(递归是彰显分治优势的工具,仅仅进行一次分治策略也许看不出优势,但递归划分到子问题规模足够小,子问题的解可用常数时间解决) - 合并 Merge
将各子问题的解合并得到原问题的解
- 划分 Divide
- 时间复杂度
- 直观估计
- 分治由以上三部分构成,整体时间复杂度则由这三部分的时间复杂度之和构成
- 由于递归,最终的子问题变得极为简单,以至于其时间复杂度在整个分治策略上的比重微乎其微
- 直观估计
分治的一个经典例子就是归并求逆序对
简单叙述一下算法:
利用归并把序列对半分,在一般的归并过程中,我们在前后两部分各设两个指针,按照大小顺序合并成一个序列
我们要做的就是在这个过程中记录逆序对个数
什么情况下会有逆序对呢?
无非是在前面的数比在后面的数大(翻译过来:在前半部分的数比在后半部分的数大)
设前半部分的指针为t1,后半部分的指针为t2
如果a[t1] > a[t2],那么t1~mid的元素一定都比t2大,一定都可以与t2形成逆序对
ans+=(mid-t1+1)+1
#include<cstdio>
#include<iostream>
#include<cstring>using namespace std;const int N=100010;
int a[N],b[N],n,ans=0; void merge(int l,int r)
{if (l==r) return;int mid=(l+r)>>1;merge(l,mid);merge(mid+1,r);int t1=l; int t2=mid+1;for (int i=l;i<=r;i++){if ((t1<=mid&&a[t1]<=a[t2])||t2>r) {b[i]=a[t1];t1++;}else{b[i]=a[t2]; ans+=(mid-t1+1);t2++;} }for (int i=l;i<=r;i++) a[i]=b[i];
}int main()
{scanf("%d",&n);for (int i=1;i<=n;i++) scanf("%d",&a[i]);merge(1,n);printf("%d",ans);return 0;
}
我为什么要介绍这个呢?
因为归并就是一个最简单的分治问题
我们在合并两个子区间的时候,要考虑到左边区间的对右边区间的影响
即,我们每次从右边区间的有序序列中取出一个元素的时候,要把“以这个元素结尾的逆序对的个数”(左边区间有多少个元素比他大)
这是一个典型的CDQ分治的过程
CDQ分治
CDQ分治是我们处理各类问题的重要武器
它的优势在于可以顶替复杂的高级数据结构,而且常数比较小;
缺点在于必须离线操作
二维偏序问题
上面介绍了归并求逆序对的经典问题,我们由此引入二维偏序问题:
给定N个有序对(a,b),求对于每个(a,b),满足a0 < a且b0 < b的有序对(a0,b0)有多少个
在归并求逆序对的时候,实际上每个元素是用一个有序对(a,b)表示的,
其中a表示数组中的位置,b表示该位置对应的值
我们求的就是“对于每个有序对(a,b),有多少个有序对(a0,b0)满足a0 < a且b0 > b”,这就是一个二维偏序问题
注意到在求逆序对的问题中,a元素是默认有序的,即我们拿到元素的时候,数组中的元素是默认从第一个到最后一个按顺序排列的,所以我们才能在合并子问题的时候忽略a元素带来的影响
因为我们在合并两个子问题的过程中,左边区间的元素一定出现在右边区间的元素之前,即左边区间的元素的a都小于右边区间元素的a
那么对于二维偏序问题,我们在拿到所有有序对(a,b)的时候,先把a元素从小到大排序
这时候问题就变成了“求顺序对”,因为a元素已经有序,可以忽略a元素带来的影响,和“求逆序对”的问题是一样的。
考虑二维偏序问题的另一种解法,用树状数组代替CDQ分治,即常用的用树状数组求顺序对
在按照a元素排序之后,我们对于整个序列从左到右扫描,每次扫描到一个有序对,求出“扫描过的有序对中,有多少个有序对的b值小于当前b值”
然而当b的值非常大的时候,空间和时间上就会吃不消,便可以用CDQ分治代替,就是我们所说的“顶替复杂的高级数据结构”
二维偏序问题的拓展
给定一个N个元素的序列a,初始值全部为0,对这个序列进行以下两种操作
操作1:格式1 x k,把位置x的元素加上k
操作2:格式为2 x y,求出区间[x,y]内所有元素的和
这是一个经典的树状数组问题
但是我们就是要没事找事,我们用CDQ分治解决它——带修改和询问的问题
我们把ta转化成一个二维偏序问题,每个操作用一个有序对(a,b)表示,其中a表示操作的时间,b表示操作的位置,时间是默认有序的,所以我们在合并子问题的过程中,就按照b从小到大的顺序合并。
首先我们把原数列和1操作都看作是修改操作
询问操作[l,r]我们拆成两个:l-1,r
因为我们询问的是一个区间和,一般的思路就是前缀和相减(我们需要具备这样的思维)
实际上我们这道题也可以这样,我们按照时间顺序进行修改
记录前缀和,当遇到l-1的标记时,我们减去sum(l-1)
遇到r标记时,询问的处理就完成了
具体流程:
- 按照id(插入位置)归并排序
- 进行左区间的修改
- 统计右区间的询问
需要注意的是:
- 在合并的时候,我们只处理左区间的修改,只统计右区间的查询
因为左区间的修改一定可以影响右区间的查询
这就体现出了CDQ分治的基本思想了 - 我们把所有操作都记录到了一个数组中,所以数组的大小至少要开到500000*3
#include<cstdio>
#include<cstring>
#include<iostream>
#define ll long longusing namespace std;const int N=5000010;
int n,m,totx=0,tot=0; //totx是操作的个数,tot询问的编号 struct node{int type,id;ll val;bool operator < (const node &a) const //重载运算符,优先时间排序 {if (id!=a.id) return id<a.id;else return type<a.type;}
};
node A[N],B[N];
ll ans[N];void CDQ(int L,int R)
{if (L==R) return;int M=(L+R)>>1;CDQ(L,M);CDQ(M+1,R);int t1=L,t2=M+1;ll sum=0; for (int i=L;i<=R;i++){if ((t1<=M&&A[t1]<A[t2])||t2>R) //只修改左边区间内的修改值{if (A[t1].type==1) sum+=A[t1].val; //sum是修改的总值B[i]=A[t1++]; }else //只统计右边区间内的查询结果{if (A[t2].type==3) ans[A[t2].val]+=sum;else if (A[t2].type==2) ans[A[t2].val]-=sum;B[i]=A[t2++];}}for (int i=L;i<=R;i++) A[i]=B[i];
}int main()
{scanf("%d%d",&n,&m);for (int i=1;i<=n;i++){tot++;A[tot].type=1; A[tot].id=i; //修改操作 scanf("%lld",&A[tot].val);}for (int i=1;i<=m;i++){int t;scanf("%d",&t);tot++;A[tot].type=t; if (t==1)scanf("%d%lld",&A[tot].id,&A[tot].val);else{int l,r;scanf("%d%d",&l,&r);totx++; A[tot].val=totx; A[tot].id=l-1; //询问的前一个位置 tot++; A[tot].type=3; A[tot].val=totx; A[tot].id=r; //询问的后端点 }}CDQ(1,tot);for (int i=1;i<=totx;i++) printf("%lld\n",ans[i]);return 0;
}
三维偏序问题
给定N个有序三元组(a,b,c),求对于每个三元组(a,b,c),有多少个三元组(a0,b0,c0)满足a0 < a且b0 < b且c0 < c
不用CDQ的算法,我们就不说了(太麻烦了)
类似二维偏序问题,先按照a元素从小到大排序,这样我们就可以忽略a元素的影响
然后CDQ分治,按照b元素从小到大进行归并排序
哪c元素我们要怎么处理呢?
这时候比较好的方案就是借助权值树状数组,
每次从左边取出三元组(a,b,c),根据c值在树状数组中进行修改
从右边的序列中取出三元组(a,b,c)时,在树状数组中查询c值小于(a,b,c)的三元组的个数
注意,每次使用完树状数组要把树状数组清零
三维偏序问题的拓展
平面上有N个点,每个点的横纵坐标在[0,1e7]之间,有M个询问,每个询问为查询在指定矩形之内有多少个点,矩形用(x1,y1,x2,y2)的方式给出,其中(x1,y1)为左下角坐标,(x2,y2)为右上角坐标
把每个点的位置变成一个修改操作,用三元组(时间,横坐标,纵坐标)来表示,把每个查询变成二维前缀和的查询
这样对于只有位于询问的左下角的修改,才对询问有影响
操作的时间是默认有序的,分治过程中按照横坐标从小到大排序,用树状数组维护纵坐标的信息
CDQ分治【分治(真得头疼)相关推荐
- bzoj2683:简单题(树状数组套CDQ分分治)
CDQ(陈丹琦)分治 CDQ显然是一个人的名字(2008NOI金牌选手陈丹琦) 这种离线的分治算法在算法界被称为"CDQ分治". 首先回忆一下归并排序的分治, 它的操作是将数组二 ...
- BZOJ 1176([Balkan2007]Mokia-CDQ分治-分治询问)
1176: [Balkan2007]Mokia Time Limit: 30 Sec Memory Limit: 162 MB Submit: 185 Solved: 94 [ Submit] ...
- 解决安卓模拟器没有网络的问题,刚开的做RN的我遇见这个问题真很头疼,所以希望看到我这篇文章能够解决。
简单来说 模拟没有网络一般是 dns 得问题 什么是dns 我这里简单得解释一下, 放在文章最后 1. 解决方案 首先 在你 安装SDK 得目录下 去找到 tools 文件夹 在目录下输入cmd如下图 ...
- 分治、CDQ分治小结
分治.CDQ分治小结 A Summary for Divide and Conquer 0. Anouncement 本文部分图片以及部分内容来自互联网,内容过多就不一一注明出处了,冒犯之处还请海涵. ...
- 分治、CDQ分治小结(need to be updated)
分治.CDQ分治小结 A Summary for Divide and Conquer 0. Anouncement 本文部分图片以及部分内容来自互联网,内容过多就不一一注明出处了,冒犯之处还请海涵. ...
- Algorithms_算法思想_递归分治
文章目录 引导案例 递归的定义 什么样的问题可以用递归算法来解决 递归如何实现以及包含的算法思 递归的公式 斐波那契数列代码实现 递归的时间复杂度和空间复杂度 递 与 归 递归的优化 优化方式一:不使 ...
- 不止代码:循环比赛(分治)
循环比赛日程表(match) [问题描述] 解析 dfs或分治 分治可以不断递归4个小正方形 左上右下为前一半,左下右上后一半 dfs就很无脑了 代码 #include<cstdio> # ...
- 分治——Secret Cow Code S(洛谷 P3612)
题目选自洛谷P3612 梳理题意 给定一个字符串,每次将其最后一个字符移到最前方,形成的新串接到原串后作为下一次操作的字符串 现询问第 N 个位置的字符 简要分析 看一眼数据范围,N<10^18 ...
- 快速排序——主要思想是分治
快速排序--主要思想是分治 分治算法的基本思想是将一个规模为N的问题分解为K个规模较小的子问题,这些子问题相互独立且与原问题性质相同.求出子问题的解,就可得到原问题的解.即一种分目标完成程序算法, ...
最新文章
- 机器人水库涵洞检测_能给眼睛打针、可水下搜索救援,手术机器人水下机器人将亮相服贸会...
- sublime Text3 设置多个浏览器预览
- TF学习——TF数据读取:TensorFlow中数据读这三张图片的5个epoch +把读取的结果重新存到read 文件夹中
- Long Number
- unity3d 随机生成地形之随机山脉
- Django视图(二)
- wordpress去掉自带的logo或者左侧栏的菜单方法
- apt-get的更新源
- 用c语言编译二叉树,C语言 数据结构平衡二叉树实例详解
- Linux之JDK安装
- html 样式初始化,css样式初始化
- 线程同步:喂,SHE
- highslide图片查看特效
- 小凡Dynamips 虚拟pc的使用
- 什么是IP地址、IP协议?
- CF#552div3题解
- python的OOP机制
- Qt 一个简单的基于mplayer的视频播放器
- 趣图:gif PostgreSQL MySQL 从删库到跑路
- JavaScript---网络编程(8)-DHTML技术演示(1)