暑假集训__cdq分治
cdq分治
一种分治方法,主要解决有着过于强大的偏序限制的可离线操作问题。
主要思想
和所有分治思想一样,将一个大问题分为两个小问题,归纳解决小问题,然后计算出其之间的贡献,即可合并为这个问题的答案。
大概是
-
找到区间[l,r]的mid
-
解决 [l,mid] , [mid+1,r] 两个小问题
-
计算左区间对于右区间贡献并更新右区间的ans
问题是如何计算两个小问题后并之后的贡献。
我们可以看一道题:
P1908 逆序对
大家肯定都学过了归并排序求逆序对
那么我们在思考一下,这其实也是一种分治:首先解决了 $ [l,mid],[mid+1,r] $,然后在合并时需要计算左边对右边的影响, 具体则是类似于双指针,右边挨个看左边有多少比它大的 ,可以参考代码理解以下过程 。
具体可看代码:
码
// 贺的
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 $ 。
点击查看代码
#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 $
点击查看代码
#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;});
// 如果你WA了,可以看看这里的排序(看不懂的可以去了解一下 lamda 表达式,GGrun懒得改了),我们想要在相同情况下严格排序,以使得所有可造成贡献的全都在他左边,此处需慎重处理
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) $
码
#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';
}
再给出一道四维偏序的例题:
其实大家可以先做一下 P4093 [HEOI2016/TJOI2016] 序列 , 可以学习一下cdq优化dp的思路。
P5621 [DBOI2019] 德丽莎世界第一可爱
首先我们已经可以处理四维偏序,但这题困难其实在于DP部分的思想和之前cdq惯用部分不同,之前是单纯的计数,先记哪个部分没有影响,但DP是有严格的转移顺序的,必须左边全都做完了,才能做右边。
也就是说,我们不能上来就处理 $ [l,mid] , [mid+1,r] $ ,因为右边部分内部转移时,并未进行左边到右边的转移,顺序是错的,事实上,应该先处理了 $ [l,mid] $ ,毕竟这部分和右边无关,然后计算左边到右边的转移,然后再处理 $ [mid+1,r] $ 的内部转移。
具体题解可以看luogu
码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int,int> pii;
#define fi first
#define se second
#define ps push_back
#define mk make_pair
#define rint register int
#define G cout<<"-------------------"<<endl
inline ll read(){
char c=getchar();ll x=0,f=1;
while(!isdigit(c))(c=='-'?f=-1:0),c=getchar();
while(isdigit(c))x=(x<<1)+(x<<3)+(c^48),c=getchar();
return x*f;
}
#define int ll
const int N=1e6+10,inf=0x7fffffff,B=1e5+1,BB=B<<1;
const ll linf=0x3f7f7f7f7f7f7f7f,mod=1e9+7;
ll f[N],w[N];
int n;
struct BIT{
ll c[BB];
inline void add(int x,ll y){
// cout<<x<<' '<<y<<endl;
while(x<BB)
c[x]=max(c[x],y),x+=x&-x;
}
inline ll ask(int x){
ll ans=-linf;
while(x)
ans=max(ans,c[x]),x^=x&-x;
return ans;
}
inline void clear(int x){
while(x<BB)
c[x]=-linf,x+=x&-x;
}
}T;
struct jj{
int a,b,c,d,op,id;
inline void print(){
cout<<a<<' '<<b<<' '<<c<<' '<<d<<' '<<op<<' '<<id<<endl;
}
}q[N],tp[N],tpp[N];
inline bool cmpb(const jj&x,const jj&y){
if(x.b!=y.b)return x.b<y.b;
if(x.c!=y.c)return x.c<y.c;
if(x.d!=y.d)return x.d<y.d;
if(x.a!=y.a)return x.a<y.a;
return w[x.id]>w[y.id];
}
inline bool cmpa(const jj&x,const jj&y){
if(x.a!=y.a)return x.a<y.a;
if(x.b!=y.b)return x.b<y.b;
if(x.c!=y.c)return x.c<y.c;
if(x.d!=y.d)return x.d<y.d;
return w[x.id]>w[y.id];
}
inline bool cmpc(const jj&x,const jj&y){
if(x.c!=y.c)return x.c<y.c;
if(x.d!=y.d)return x.d<y.d;
if(x.a!=y.a)return x.a<y.a;
if(x.b!=y.b)return x.b<y.b;
return w[x.id]>w[y.id];
}
inline void man(int l,int r){
if(l==r)return;
int mid=l+r>>1;
man(l,mid);
for(int i=l;i<=r;++i)
tpp[i]=tp[i];
stable_sort(tpp+l,tpp+mid+1,cmpc);
stable_sort(tpp+mid+1,tpp+r+1,cmpc);
for(int j=mid+1,i=l;j<=r;++j){
while(i<=mid&&tpp[i].c<=tpp[j].c){
if(!tpp[i].op)T.add(tpp[i].d,f[tpp[i].id]);
++i;
}
if(tpp[j].op)f[tpp[j].id]=max(f[tpp[j].id],T.ask(tpp[j].d)+w[tpp[j].id]);
}
for(int i=l;i<=mid;++i)
T.clear(tpp[i].d);
man(mid+1,r);
}
inline void cdq(int l,int r){
if(l==r)return;
int mid=l+r>>1;
cdq(l,mid);
for(int i=l;i<=r;++i)
tp[i]=q[i],tp[i].op=(i>mid);
stable_sort(tp+l,tp+r+1,cmpb);
man(l,r);
cdq(mid+1,r);
}
main(){
#ifndef ONLINE_JUDGE
freopen("in.in","r",stdin);
freopen("out.out","w",stdout);
#endif
ios::sync_with_stdio(0);cin.tie(0),cout.tie(0);
n=read();
memset(T.c,0xcf,sizeof(T.c));
for(int i=1,a,b,c,d,z;i<=n;++i){
a=read()+B,b=read()+B,c=read()+B,d=read()+B,z=read();q[i]={a,b,c,d,0,i};f[i]=z;w[i]=z;
}
stable_sort(q+1,q+1+n,cmpa);
cdq(1,n);
for(int i=2;i<=n;++i)
f[1]=max(f[1],f[i]);
cout<<f[1];
}

浙公网安备 33010602011771号