【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 码量小,常数小,还是很有优势的。
三千 铁衣披霜 万籁绝响 举目是残阳
回首 剑拔弩张 箭已经在弦上
耳畔 江海有声 山河无量 呼万寿无疆
却无人共看 这人间多荒唐

浙公网安备 33010602011771号