cdq分治
一种分治方法,主要解决有着过于强大的偏序限制 的可离线操作问题。
主要思想
和所有分治思想一样,将一个大问题分为两个小问题,归纳解决小问题,然后计算出其之间的贡献,即可合并为这个问题的答案。
大概是
-
找到区间l,r的mid
-
解决 l,mid , mid+1,r 两个小问题
-
计算左区间对于右区间贡献并更新右区间的ans
问题是如何计算两个小问题后并之后的贡献。
我们可以看一道题:
P1908 逆序对
大家肯定都学过了归并排序求逆序对
那么我们在思考一下,这其实也是一种分治:首先解决了 \[l,mid\],\[mid+1,r\] ,然后在合并时需要计算左边对右边的影响, 具体则是类似于双指针,右边挨个看左边有多少比它大的 ,可以参考代码理解以下过程 。
具体可看代码:
码
cpp
// 贺的
inline 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,now=l;//i,j表示左右指针,now表示b数组的指针
while(i<=mid&&j<=r){
if(a[i]<=a[j])b[now++]=a[i++];//不产生贡献
else{//产生贡献
b[now++]=a[j++];
ans+=mid-i+1;//计算贡献
}
}
while(i<=mid)b[now++]=a[i++];
while(j<=r)b[now++]=a[j++];
for(int i=l;i<=r;i++)a[i]=b[i];
return;
}
\\\
首先我们对 \[l,mid\],\[mid+1,r\] 进行完操作后,他们应该是已经排好序的。
如 1,4,6 , 2,3,8
初始状态,双指针都在最开始, i -\> l , j -\> mid+1

首先比较 i 和 j 两指针所指向的谁比较小,可以看到 i 所指的比较小 ,左侧不需要考虑新贡献,所以只需要将其加入b里面以方便排序,将 i 向右移动一格。

再次进行比较,发现 j 所指的比较小,需要计算贡献,此时 \> j 的有 mid - i + 1 个,加在 ans 上即可,然后将 j 所指的加到 b 数组里即可 , j 右移。
然后依次进行比较:
- j
- i
- i
- j
最后输出 ans 即可 , 复杂度 O ( n \\log n )
那么我们大概可以理解cdq的主要思想了,接下来看几道例题。
例题
P4390 BalkanOI 2007 Mokia 摩基亚
首先不难想到一个矩形 \[a,b\] , \[c,d\] , ( a \<= c , b \<= d ) , 可以拆成 \[c,d\] , \[c,b-1\] , \[a-1,d\] , \[a-1,b-1\] 四个与 \[1,1\] 组合的大矩形计算。
于是题目转化为了,给出对任意 \[x,y\] 的操作和询问,涉及二维偏序(其实还有时间一维)。
类似于上面,可以在cdq双指针里套一层树状数组解决,可以做到 n \\log \^2 n 。
关于时间复杂度的证明: 首先cdq本身是一个log,在每个双指针进行操作时,我们将直接操作改为树状数组,相当于每个操作都乘了一个 \\log m , m 是不确定的,但 m \\le n ,所以其实复杂度是介于 n\\log n , n \\log\^2 n 之间的,但基本认为是近似于 n \\log\^2 n 。
点击查看代码
cpp
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int,int> pii;
#define fi first
#define se second
#define ps push_back
#define mk make_pair
// #define int ll
const int N=2e5+10,inf=0x3f3f3f3f,mod=1e9+7;
inline ll read(){
char c=getchar();ll x=0;bool f=0;
while(!isdigit(c))f=c=='-'?1:0,c=getchar();
while(isdigit(c))x=(x<<1)+(x<<3)+(c^48),c=getchar();
return f?-x:x;
}
mt19937 rd(233);
int s,w,cnt,num;
struct jj{
int a,b,op,id;
inline void out(){cout<<a<<' '<<b<<' '<<op<<' '<<id<<endl;}
}q[N],tp[N];
int c[N*10],ans[N];
inline void add(int x,int y){while(x<=w)c[x]+=y,x+=x&-x;}
inline int ask(int x){int ans=0;while(x)ans+=c[x],x^=x&-x;return ans;}
inline 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=l;
while(i<=mid&&j<=r){
if(q[i].a<=q[j].a){
if(q[i].op>0)add(q[i].b,q[i].op);
tp[k++]=q[i++];
}
else{
if(q[j].op==-1)ans[q[j].id]+=ask(q[j].b);
else if(q[j].op==-2)ans[q[j].id]-=ask(q[j].b);
tp[k++]=q[j++];
}
}
while(j<=r){
if(q[j].op==-1)ans[q[j].id]+=ask(q[j].b);
else if(q[j].op==-2)ans[q[j].id]-=ask(q[j].b);
tp[k++]=q[j++];
}
for(int p=l;p<i;++p)if(q[p].op>0)add(q[p].b,-q[p].op);
while(i<=mid)tp[k++]=q[i++];
while(l<=r)q[l]=tp[l],++l;
}
signed main(){
s=read(),w=read();
int op=0,x,y,xx,yy;
while(op!=3){
op=read();
if(op==1)x=read(),y=read(),xx=read(),q[++cnt]={x,y,xx,0};
else if(op==2)x=read(),y=read(),xx=read(),yy=read(),q[++cnt]={xx,yy,-1,++num},q[++cnt]={xx,y-1,-2,num},q[++cnt]={x-1,yy,-2,num},q[++cnt]={x-1,y-1,-1,num},ans[num]=(xx*yy-(xx*(y-1))-((x-1)*yy)+((x-1)*(y-1)))*s;
}
cdq(1,cnt);
for(int i=1;i<=num;++i)
cout<<ans[i]<<'\n';
}
P3810 【模板】三维偏序 / 陌上花开
其实同样这次是真的三维偏序,只需先sort解决一维,然后和上面一样cdq+树状数组干掉两维。
需要注意的是,挂=就很难处理,一个不错的思路是分为操作和询问分类进行处理,在sort里优先将操作放在前面以产生对后面的影响,最后答案-1即可 , n \\log \^2 n
点击查看代码
cpp
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int,int> pii;
#define fi first
#define se second
#define ps push_back
#define mk make_pair
const int N=2e5+10,inf=0x3f3f3f3f,mod=1e9+7;
inline ll read(){
char c=getchar();ll x=0;bool f=0;
while(!isdigit(c))f=c=='-'?1:0,c=getchar();
while(isdigit(c))x=(x<<1)+(x<<3)+(c^48),c=getchar();
return f?-x:x;
}
mt19937 rd(233);
int n,k;
struct jj{
int a,b,c,ans,op;
//op : 1 -> ask, 0-> do
inline void out(){cout<<a<<' '<<b<<' '<<c<<' '<<ans<<endl;}
}q[N],qq[N];
int c[N],anss[N];
inline void add(int x,int v){while(x<=k)c[x]+=v,x+=x&-x;}
inline int ask(int x){int ans=0;while(x)ans+=c[x],x^=x&-x;return ans;}
inline 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,kk=l;
while(j<=r){
while(i<=mid&&q[i].b<=q[j].b){if(!q[i].op)add(q[i].c,1);qq[kk++]=q[i++];}
if(q[j].op){
q[j].ans+=ask(q[j].c);
}
qq[kk++]=q[j++];
}
for(int p=l;p<i;++p)
if(!q[p].op){add(q[p].c,-1);}
while(i<=mid)qq[kk++]=q[i++];
while(l<=r)q[l]=qq[l],++l;
}
int main(){
#ifndef ONLINE_JUDGE
freopen("in.in","r",stdin);
freopen("out.out","w",stdout);
#endif
n=read(),k=read();
for(int i=1;i<=n;++i)
q[i*2-1]={read(),read(),read(),0,0},q[i<<1]=q[(i<<1)-1],q[i<<1].op=1;
sort(q+1,q+1+n*2,[](jj x,jj y){return x.a==y.a?x.b==y.b?x.c==y.c?x.op<y.op:x.c<y.c:x.b<y.b:x.a<y.a;});
cdq(1,n*2);
for(int i=1;i<=n*2;++i){
q[i].ans-=1;
if(q[i].op){
++anss[q[i].ans];
}
}
for(int i=0;i<n;++i)
cout<<anss[i]<<'\n';
}
进阶(从三维偏序到四维偏序)
根据上面的题目,我们似乎只会最多三维偏序,接下来学习四维偏序怎么处理。
P14957 【模板】离线静态四维数点
首先转化一下题意,可以将 x2 , y2 取负,这样我们的条件都是 x \\le X , y \\le Y 这样后面也不用考虑谁大谁小了。
事实上,cdq是可以嵌套的。
啥意思? 其实我们可以想一下,对于计数的条件是四维都满足 \\le 的关系,那么我们首先 sort + cdq 解决一维,然后变成了三维,如果再套一层 cdq + 双指针 + 树状数组 即可,可是从第一层cdq如何转向第二层cdq,并保证第一层的cdq所排序的信息有效。
在 cdq1 时,我们首先递归解决了 \[l,mid\] , \[mid+1,r\] 接下来如何计算左边对右边的影响? 我们知道,经过递归,左右序列均已按照 y1 (这里我定义 y1 为第二关键字,别的也可以) 排好序了,并且第一层的排序使得只能左边对右边产生影响,剩下的两维需要传到 cdq2 里面去解决。
可是在cdq2之前,我们已经按第二关键字,排完序了,我们需要保住第一层的信息,即 区分好左边和右边 ,所以我们在进入cdq2之前要先给 \[l,mid\] 部分打上 左 标记 , 给 \[mid+1,r\] 打上 右 标记,然后传入cdq2后只能由既有左标记,又在新排序后左侧 的数给既有右标记,又在新排序后右侧的数造成贡献。这样,在同时保证一二维的情况下可以用双指针加树状数组解决后两维,复杂度 O (n \\log\^3 n)
码
cpp
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int,int> pii;
#define fi first
#define se second
#define ps push_back
#define mk make_pair
const int N=8e5+10,inf=0x3f3f3f3f,mod=1e9+7;
inline ll read(){
char c=getchar();ll x=0;bool f=0;
while(!isdigit(c))f=c=='-'?1:0,c=getchar();
while(isdigit(c))x=(x<<1)+(x<<3)+(c^48),c=getchar();
return f?-x:x;
}
struct jj{
int x1,y1,x2,y2,op;
bool man;
inline void out(){cout<<x1<<' '<<y1<<' '<<x2<<' '<<y2<<' '<<op<<' '<<man<<endl;}
// op : 0->do 1->ask
// man : 0->l 1->r
}q[N],tp[N];
int n,m,ans[N],cnt;
unordered_map<int,int> ma;
inline bool cmp(jj x,jj y){
return x.x1==y.x1?x.y1==y.y1?x.x2==y.x2?x.y2==y.y2?x.op<y.op:x.y2<y.y2:x.x2<y.x2:x.y1<y.y1:x.x1<y.x1;
}
inline bool cmp1(jj x,jj y){
return x.y1==y.y1?x.x2==y.x2?x.y2==y.y2?x.op<y.op:x.y2<y.y2:x.x2<y.x2:x.y1<y.y1;
}
int c[N];
inline void add(int x,int v){while(x<=cnt)c[x]+=v,x+=x&-x;}
inline int ask(int x){int ans=0;while(x)ans+=c[x],x^=x&-x;return ans;}
inline void sol(int l,int r){
if(l==r)return;
int mid=l+r>>1;
sol(l,mid),sol(mid+1,r);
int i=l,j=mid+1,k=l;
while(i<=mid&&j<=r){
if(q[i].x2<=q[j].x2){
if(!q[i].man&&!q[i].op)add(q[i].y2,1);
tp[k++]=q[i++];
}
else{
if(q[j].man&&q[j].op)ans[q[j].op]+=ask(q[j].y2);
tp[k++]=q[j++];
}
}
while(j<=r){
if(q[j].man&&q[j].op)ans[q[j].op]+=ask(q[j].y2);
tp[k++]=q[j++];
}
for(int p=l;p<i;++p)
if(!q[p].man&&!q[p].op)add(q[p].y2,-1);
while(i<=mid)tp[k++]=q[i++];
while(l<=r)q[l]=tp[l],++l;
}
inline void cdq(int l,int r){
if(l==r)return;
int mid=l+r>>1;
cdq(l,mid),cdq(mid+1,r);
for(int i=l;i<=mid;++i)
q[i].man=0;
for(int i=mid+1;i<=r;++i)
q[i].man=1;
stable_sort(q+l,q+r+1,cmp1);
sol(l,r);
}
vector<int> v;
int main(){
#ifndef ONLINE_JUDGE
freopen("in.in","r",stdin);
freopen("out.out","w",stdout);
#endif
n=read(),m=read();
for(int i=1;i<=n;++i)
q[i].x1=read(),q[i].y1=read(),q[i].x2=-read(),q[i].y2=-read(),q[i].op=0;
for(int i=1;i<=m;++i)
q[i+n].x1=read(),q[i+n].y1=read(),q[i+n].x2=-read(),q[i+n].y2=-read(),q[i+n].op=i;
for(int i=1;i<=n+m;++i)
v.ps(q[i].y2);
sort(v.begin(),v.end());unique(v.begin(),v.end());
for(auto i:v)
ma[i]=++cnt;
for(int i=1;i<=n+m;++i)
q[i].y2=ma[q[i].y2];
sort(q+1,q+1+n+m,cmp);
cdq(1,n+m);
for(int i=1;i<=m;++i)
cout<<ans[i]<<'\n';
}