论CDQ分治
前言
需要的前置知识点:归并排序,树状数组(或线段树),偏序问题的定义。
归并排序求逆序对(极其重要,会了这个其实你就可以不用看了因为这个就是cdq分治,当然不会也可以看因为没有用到这个)
二维偏序:二维数点问题
问题是,坐标系第一象限有若干个点,求每个点左下角的点数。
解法:排序排一维,然后第二维上数据结构,比如树状数组或线段树,因为排序保证了 x x x坐标的顺序,我们在树状数组里查 y y y小于当前点的就可以了。
复杂度 n l o g n nlogn nlogn
可以发现排序很好使,直接解决了一维,那能不能解决两维呢?看似不能,因为 x , y x,y x,y很难同时升序,但是实际上是可以的,方法就是cdq分治
问题转化
假设我们有两个区间,都是有序的,对于区间二的每一个数 x x x,求出区间一中有多少个 y y y比它小,这个怎么做?双指针。
那么回归刚才的问题,我们也想用这个解法,为什么这个很好呢?考虑将原序列划分为两个区间,保证区间一的 x x x更小,区间内按 y y y排序,然后双指针,就可以做了。
但是问题是,区间一对区间二的贡献计算全了,那区间二内部的呢?
当你问出这个的时候,你发现这个等价于原问题 了,递归下去,做完了,这就是cdq分治。
算法实现
当然细节还是要说一说的,我们发现左右区间中 y y y局部有序,而且 x x x满足左区间任取都小于等于右区间,如果我们把 x x x看成下标,你发现这个性质完美地符合归并排序。
你还记得归并排序求逆序对吗,那个的本质就是cdq分治
因为逆序对可以归约到二维偏序,然后下标不用排天然有序,数值使用归并排一下。
实现方式就是按归并排序递归,然后合并的时候,如果右侧值小,就把那个点的答案计算一下,然后接着做就都一样了。
时间复杂度 n l o g n nlogn nlogn,同归并排序
从二维偏序到三维偏序
我就是喜欢数据结构如果学了cdq还能用到吗?
能的,这两个可以一起用。
对于偏序问题,有一个基本思路,就是计算维度。
排序 = 1维
cdq = 2维(就是加强版的排序)
树状数组/线段树 = 1维
树套树 = 2维
计算一下,cdq+树状数组 = 2+1 = 3
那么我们把问题换成三维数点
先按 z z z排序,这个时候在递归的结构里 z z z天然有序,你只需解决两个区间之间的二维偏序即可。
区间内按 x x x排序, x x x那维同样解决,现在你发现, y y y的限制不一定满足,原本是左区间将数插入新序列就要计算答案,但现在不一定了,怎么办?
直接扔进树状数组,然后右区间插入时按 y y y查询即可。
时间复杂度怎样呢?我们不直接清空树状数组,而是一个一个点删除,这样每个点进来一次就会被删除一次, n n n个点,每个点在 l o g n logn logn层中出现,即遍历 l o g n logn logn次,每次遍历到都要花 l o g n logn logn的时间插入或删除。
总计 n l o g 2 n nlog^2n nlog2n
给个代码吧(luogu P3810)
cpp
#include<bits/stdc++.h>
using namespace std;
const int N = 200010;
int n,k,cnt = 0;
int ans[N];
inline int lowbit(int x){
return x&-x;
}
struct BITtree{
int num[N];
void update(int x,int y){
for(int i = x;i<=k;i+=lowbit(i)){
num[i]+=y;
}
}
int query(int x){
int res = 0;
for(int i = x;i>0;i-=lowbit(i)){
res+=num[i];
}
return res;
}
}tr;
struct node{
int x,y,z;
int num,ans;
}a[N],b[N];
bool cmp1(node a,node b){
if(a.z!=b.z)return a.z<b.z;
else if(a.x!=b.x)return a.x<b.x;
else return a.y<b.y;
}
queue<pair<int,int>> del;
void cdq(int l,int r){
if(l==r)return;
int mid = (l+r)>>1;
cdq(l,mid);
cdq(mid+1,r);
for(int p = 1,i = l,j = mid+1;p<=r-l+1;p++){
if(j>r||(i<=mid&&a[i].x<=a[j].x)){
b[p] = a[i];
tr.update(b[p].y,b[p].num);
del.push({b[p].y,b[p].num});
i++;
}else{
b[p] = a[j];
b[p].ans+=tr.query(b[p].y);
j++;
}
}
while(!del.empty()){
tr.update(del.front().first,-del.front().second);
del.pop();
}
for(int i = l,j = 1;i<=r;i++,j++){
a[i] = b[j];
}
}
int main(){
cin>>n>>k;
for(int i = 1;i<=n;i++){
cin>>b[i].x>>b[i].y>>b[i].z;
b[i].ans = b[i].num = 0;
}
sort(b+1,b+n+1,cmp1);
for(int i = 1;i<=n;i++){
if(cnt==0||b[i].x!=b[i-1].x||b[i].y!=b[i-1].y||b[i].z!=b[i-1].z){
cnt++;
a[cnt] = b[i];
}
a[cnt].num++;
}
cdq(1,cnt);
for(int i = 1;i<=cnt;i++){
ans[a[i].ans+a[i].num-1]+=a[i].num;
}
for(int i = 0;i<n;i++){
cout<<ans[i]<<"\n";
}
return 0;
}
其他应用
你要是刚学就不要看这里了,先把二维偏序和三维偏序的板子写了,理解理解,要不看这个有点困难
问题就是在静态数组中求区间mex,多组询问(luogu P4137)
我会主席树!
恭喜你秒了,但是我们不用数据结构。
首先我们发现,值域很大,但是屁用没有,一定会有一个 x ≤ n x \le n x≤n,满足 x x x不存在,那么大于 x x x的数没用了,直接扔。
然后我们发现,如果原序列是排列(即在上述情况基础上,没有重复的),区间mex等于补集min ,你会这个之后2026联合省选D2T1就秒了,但是这还不够,重复是难免的,补集里有,区间里可能还有。
这个时候不好做,但是我们可以发现一个性质,对于一个数 x x x,原序列中有若干个区间没有 x x x,只要我们的区间是这个区间的子区间,那么它就没有 x x x。
好了,我们现在有思路了,对于每个 x x x,假设它有 k k k个,那你就划分出 k + 1 k+1 k+1个区间,设这个区间左右端点分别为 p p p和 q q q,你要求所有 p p p小于等于 l l l且 q q q大于等于 r r r的区间中, x x x的最小值,显然,二维偏序,上cdq,先排 l l l和 p p p,然后在递归中局部排 q q q和 r r r,马上就做完了。
没啥用,感觉不如主席树。
但是cdq的题就是考验问题转化,你如果能早点做这题,省选D2T1不就秒了吗(虽然各位神犇本来也能秒)
后记
cdq分治没有数据结构好想,还不怎么考,但是你学了它一定不会后悔的。
二维的点阵,照理来说无法排序,但是cdq分治就可以一会排 x x x一会排 y y y,利用局部的有序解决问题。
总之就是cdq分治牛逼!
本文作者是蒟蒻,如有错误请各位神犇指点
森林古猿出品,必属精品,请认准CSDN森林古猿1!