CDQ分治入门
CDQ分治
cdq分治与其说是算法,不如说是一种解决问题的思想或方式。与常规分治对比,常规分治是通过解决左右两个子区间的问题,合并得到大区间问题的解。但cdq分治通过处理左区间对右区间的影响来得到大区间的解。一般来说,cdq分治可以顶替一层数据结构,而且常数小,空间开销也小。缺点是需要离线。
离线为前提
一般来说,支持离线是cdq分治可行的前提,因为我们要用合理的顺序解决操作和询问的关系,处理左边对右边的影响,逐步得到所有答案。如果碰到一些强制在线的题目,一般cdq分治是做不了的。
分治为基础
以几道经典问题来逐步介绍CDQ分治的思想。
归并排序来求解一个序列的逆序对个数,
这个基础算法其实就体现了CDQ的思想。在分治的基础上,处理左边对右边的逆序对贡献。这个太基础,不赘述
二维偏序问题
存在n个二元组(a,b),问存在多少对i,j使得ai<=aj&&bi<=bj。这个问题也可以转化成上述问题。对n个二元组,按第一维升序排列,以忽略a的影响。这时问题就转化成新序列有多少个顺序对了。
单点加、区间求和问题
两种操作,对位置P加C或询问[L,R]的和。这本该是树状数组解决的问题,但为了学习cdq,我们对其进行转化,也可以转化成上述类似问题。我们这样看待操作,区间求和转化为两个前缀求和的差,以操作时间为第一维,操作的位置为第二维。按第一维时间来排序(一般就是读入的顺序),这样就只考虑操作位置。可以发现只有位置靠左的修改才对位置靠右的前缀求和询问产生影响。左边对右边的贡献就是那些修改的值。至于初始值,也可以用修改操作来赋上。
具体实现:
#include<bits/stdc++.h>
#define dd(x) cout<<#x<<" = "<<x<<" "
#define de(x) cout<<#x<<" = "<<x<<"\n"
#define sz(x) int(x.size())
#define All(x) x.begin(),x.end()
#define pb push_back
#define mp make_pair
#define fi first
#define se second
using namespace std;
typedef long long ll;
typedef long double ld;
typedef pair<pair<int,int>,int> P;
typedef priority_queue<int> BQ;
typedef priority_queue<int,vector<int>,greater<int> > SQ;
const int maxn=5e5+10,mod=1e9+7,INF=0x3f3f3f3f;
P op[maxn<<2],tmp[maxn<<2];// ( (pos, type), v) //type,v为附加信息,若为操作1,type=1,v是修改的值;若为操作2,type=2或3,v是第几个询问
int cnt,id;
ll ans[maxn];
void cdq(int l,int r)
{
if (l>=r)
return;
int mid=(l+r)>>1;
cdq(l,mid);
cdq(mid+1,r);
int i=l,j=mid+1,k=0;
ll sum=0;
while (i<=mid&&j<=r)
{
if (op[i]<op[j])
{
if (op[i].fi.se==1)
sum+=op[i].se;
tmp[k++]=op[i++];
}
else
{
if (op[j].fi.se==2)
ans[op[j].se]-=sum;
else if (op[j].fi.se==3)
ans[op[j].se]+=sum;
tmp[k++]=op[j++];
}
}
while (i<=mid)
tmp[k++]=op[i++];
while (j<=r)
{
if (op[j].fi.se==2)
ans[op[j].se]-=sum;
else if (op[j].fi.se==3)
ans[op[j].se]+=sum;
tmp[k++]=op[j++];
}
for (i=0;i<k;++i)
op[l+i]=tmp[i];
}
int main()
{
int n,q;
cin>>n>>q;
for (int i=1;i<=n;++i)
{
scanf("%d",&op[++cnt].se);
op[cnt].fi.fi=i;
op[cnt].fi.se=1;
}
while (q--)
{
int ty,x,y;
scanf("%d%d%d",&ty,&x,&y);
if (ty==1)
op[++cnt].fi.fi=x,op[cnt].fi.se=1,op[cnt].se=y;
else
{
op[++cnt].fi.fi=x-1,op[cnt].fi.se=2,op[cnt].se=++id;
op[++cnt].fi.fi=y,op[cnt].fi.se=3,op[cnt].se=id;
}
}
cdq(1,cnt);
for (int i=1;i<=id;++i)
printf("%lld\n",ans[i]);
return 0;
}
二维偏序问题的变形
附上一道二维偏序的简单变形园丁的烦恼,二维空间下,事先给定n个点,然后有m个查询,问指定矩形内含多少个点。由于这题一开始会把所有的点加入,然后再查询,因此不存在时间维度的考虑,所以实际是转化为二维偏序的问题。一个矩形求和可以转化为四个矩形前缀求和(随便拿张纸,画一下,大概就是根据容斥的思想对四个矩形前缀加加减减得到要求的矩形和)。而求矩形前缀和就变成了上述的二维偏序问题了。
代码:
#include<bits/stdc++.h>
#define dd(x) cout<<#x<<" = "<<x<<" "
#define de(x) cout<<#x<<" = "<<x<<"\n"
#define sz(x) int(x.size())
#define All(x) x.begin(),x.end()
#define pb push_back
#define mp make_pair
#define fi first
#define se second
using namespace std;
typedef long long ll;
typedef long double ld;
typedef pair<pair<int,int>,pair<int,int> > P;
typedef priority_queue<int> BQ;
typedef priority_queue<int,vector<int>,greater<int> > SQ;
const int maxn=4e6+10,mod=1e9+7,INF=0x3f3f3f3f;
struct node
{
int x,y,ty,id;
bool operator <(const node& t)
{
return x==t.x? (ty<t.ty):(x<t.x);
}
};
node p[maxn],tmp[maxn];
int ans[maxn>>2];
void cdq(int l,int r)
{
if (l>=r)
return;
int m=(l+r)>>1;
cdq(l,m);
cdq(m+1,r);
int i=l,j=m+1,k=0,sum=0;
while (i<=m&&j<=r)
{
if (p[i].y<=p[j].y)
{
if (!p[i].ty)
sum++;
tmp[k++]=p[i++];
}
else
{
if (p[j].ty)
ans[p[j].id]+=p[j].ty==1?sum:-sum;
tmp[k++]=p[j++];
}
}
while (i<=m)
tmp[k++]=p[i++];
while (j<=r)
{
if (p[j].ty)
ans[p[j].id]+=p[j].ty==1?sum:-sum;
tmp[k++]=p[j++];
}
for (i=0;i<k;++i)
p[l+i]=tmp[i];
}
int main()
{
int n,m;
cin>>n>>m;
for (int i=1;i<=n;++i)
scanf("%d%d",&p[i].x,&p[i].y);
int cnt=n;
for (int i=1;i<=m;++i)
{
int x1,y1,x2,y2;
scanf("%d%d%d%d",&x1,&y1,&x2,&y2);
x1--,y1--;
p[++cnt].x=x1,p[cnt].y=y1,p[cnt].ty=1,p[cnt].id=i;
p[++cnt].x=x2,p[cnt].y=y2,p[cnt].ty=1,p[cnt].id=i;
p[++cnt].x=x1,p[cnt].y=y2,p[cnt].ty=2,p[cnt].id=i;
p[++cnt].x=x2,p[cnt].y=y1,p[cnt].ty=2,p[cnt].id=i;
}
sort(p+1,p+1+cnt);
cdq(1,cnt);
for (int i=1;i<=m;++i)
printf("%d\n",ans[i]);
return 0;
}
三维偏序问题
也就是二位偏序的基础上,再加一维c。常规的做法有树套树,但空间开销较大。因此可以用cdq来顶掉一层数据结构。思想是逐步降维,做法大概是对第一维排序,忽略a的影响。然后对第二维b进行cdq分治。但因为多了一维c,那么发现左边元素的b小于右边元素的b时,贡献就不是简单的+1,要考虑c。因此维护一个树状数组来统计c。左边对右边有贡献时,贡献在c的位置。统计右边元素时,统计的是小于右边元素c的个数(因为此时a,b的影响都已经处理掉了,只需要考虑c)。记得每次用完要清空树状数组。
以洛谷上一道三维偏序模板题为例陌上花开
#include<bits/stdc++.h>
#define dd(x) cout<<#x<<" = "<<x<<" "
#define de(x) cout<<#x<<" = "<<x<<"\n"
#define sz(x) int(x.size())
#define All(x) x.begin(),x.end()
#define pb push_back
#define mp make_pair
#define fi first
#define se second
using namespace std;
typedef long long ll;
typedef long double ld;
typedef pair<int,pair<int,int> > P;//(a, (b,c))
typedef priority_queue<int> BQ;
typedef priority_queue<int,vector<int>,greater<int> > SQ;
const int maxn=1e5+10,mod=1e9+7,INF=0x3f3f3f3f;
struct node
{
P p;
int w,cnt;
}a[maxn],tmp[maxn];
int fwk[maxn<<1],ans[maxn];
void add(int p,int c)
{
for (int i=p;i<(maxn<<1);i+=i&-i)
fwk[i]+=c;
}
int qry(int p)
{
int res=0;
for (int i=p;i;i-=i&-i)
res+=fwk[i];
return res;
}
void cdq(int l,int r)
{
if (l>=r)
return;
int m=(l+r)>>1;
cdq(l,m);
cdq(m+1,r);
int i=l,j=m+1,k=0;
while (i<=m&&j<=r)
{
if (a[i].p.se.fi<=a[j].p.se.fi)
{
add(a[i].p.se.se,a[i].w);
tmp[k++]=a[i++];
}
else
{
a[j].cnt+=qry(a[j].p.se.se);
tmp[k++]=a[j++];
}
}
while (j<=r)
{
a[j].cnt+=qry(a[j].p.se.se);
tmp[k++]=a[j++];
}
for (int t=l;t<i;++t)
add(a[t].p.se.se,-a[t].w);
while (i<=m)
tmp[k++]=a[i++];
for (i=0;i<k;++i)
a[l+i]=tmp[i];
}
int main()
{
int n,k;
cin>>n>>k;
for (int i=1;i<=n;++i)
{
scanf("%d%d%d",&a[i].p.fi,&a[i].p.se.fi,&a[i].p.se.se);
a[i].p.se.se++;
}
sort(a+1,a+1+n,[&](node& x,node& y){return x.p<y.p;});
int cnt=0,w=1;
for (int i=1;i<=n;++i)
{
if (a[i].p==a[i+1].p&&i<n)
++w;
else
a[++cnt].p=a[i].p, a[cnt].w=w, a[cnt].cnt=w-1, w=1;
}
cdq(1,cnt);
for (int i=1;i<=cnt;++i)
ans[a[i].cnt]+=a[i].w;
for (int i=0;i<n;++i)
printf("%d\n",ans[i]);
return 0;
}