cdq分治学习笔记(非教学向)

cdq分治学习笔记(非教学向)

(非完全版,待更)

  1. cdq分治是什么?

cdq分治就是将一个大问题分解成两个规模是它一半的子问题,和两个子问题之间的答案统计的算法,复杂度$ nklogn $

  1. cdq分治怎么写?

详见例题代码

  1. cdq分治可以干什么?

在我目前的理解中,cdq分治是用来解决一些和偏序有关的题目的,就比如说一个点的答案只能从一个一些属性都比它小的点转移来,这时就可以用cdq分治去更新答案了

推荐题目:

t1.

【模板】三维偏序(陌上花开)

cdq分治模板题,前置芝士:cdq分治,树状数组

//I love Nanami Chiaki
#include<bits/stdc++.h>
using namespace std;
#define lowbit(x) x&(-x)
const int maxn=1e5+7;
int n,k;
int ans[maxn],res[maxn];
int dat[maxn<<1];
bool hve[maxn];
struct Flower{
    int a,b,c,id;
}f[maxn],t[maxn];
inline bool cmp1(Flower X,Flower Y){
    return (X.a^Y.a)?(X.a<Y.a):((X.b^Y.b)?(X.b<Y.b):(X.c<Y.c));
}
inline void add(int p,int v){
    while (p<=k){
        dat[p]+=v;
        p+=lowbit(p);
    }
}
inline int sum(int p){
    int re=0;
    while (p){
        re+=dat[p];
        p-=lowbit(p);
    }
    return re;
}
void cdq1(int l,int r){
    if (l==r) return;
    int mid=(l+r)>>1;
    cdq1(l,mid);
    cdq1(mid+1,r);
    int i=l,j=mid+1,sz=l;
    while (i<=mid || j<=r){
        if (i>mid){
            t[sz++]=f[j];
            ans[f[j].id]+=sum(f[j].c);
            j++;            
        }
        else if (j>r || f[i].b<=f[j].b){
            t[sz++]=f[i];
            hve[f[i].id]=true;
            add(f[i++].c,1);
        }
        else{
            t[sz++]=f[j];
            ans[f[j].id]+=sum(f[j].c);
            j++;
        }
    }
    for (int i=l;i<=r;i++){
        f[i]=t[i];
        if (hve[f[i].id]){
            hve[f[i].id]=false;
            add(f[i].c,-1);
        }
    }
}
int main(){
    scanf("%d%d",&n,&k);
    for (int i=0;i<n;i++){scanf("%d%d%d",&f[i].a,&f[i].b,&f[i].c);f[i].id=i;}
    sort(f,f+n,cmp1);
    int prea=-1,preb=-1,prec=-1,num=0;
    for (int i=n;i>=0;i--){
        if (f[i].a==prea && f[i].b==preb && f[i].c==prec) ans[f[i].id]=num++;
        else{
            prea=f[i].a;preb=f[i].b;prec=f[i].c;num=1;
        }
    }
    cdq1(0,n-1);
    for (int i=0;i<n;i++) res[ans[i]]++;
    for (int i=0;i<n;i++) printf("%d\n",res[i]);
    return 0;
}

大概说一下自己看题解/写程序时的疑惑

  1. 为什么这么写是对的?

    • 这是我最直观的疑问,思考了一会之后,我得出了答案。

    • 在main函数里的时候,我们已经保证了属性a有序(仔细看sort里的cmp函数会发现b和c也要参与排序),这保证了答案一定是由前面的数去更新后面的数(后面的数不可能去更新前面的数?有一个特殊情况,等一会再说),这大概是cdq分治的基础。

    • 然后在函数cdq1中,我们先分治,听说好像有先不分治的题,但我好像还没遇到,毕竟我才做了2题,仔细观察while循环发现,原来返回的f数组竟然是根据属性b排序后的结果,这之中就有个很妙的地方。

      扫描二维码关注公众号,回复: 8336366 查看本文章
    • 这样子a不会乱序吗?是会乱序的,但没什么关系,两个子问题之间内部之间的答案更新已经被处理完了,剩下来的就是子问题之间的答案更新,由于我们一开始排过序了,所以前面的数一定满足属性a比后面的数小或等于,也就意味着即使两个子问题的a都乱序了,但前面一个子问题的a一定都比后面一个子问题的a小或等于。

    • 这个时候将两个对b有序的子问题合并,在合并的过程中先把b小的添加的新的序列里去,并用树状数组维护前面一个子问题c=k的有多少个,同时对于每一个后面子问题的点,他的答案个数就要加上树状数组中c比它小的个数就可以了(这是因为在树状数组中的数一定满足b比它小(因为比它添加的早),且a比它小,前面说过原因了)。

为了节省大家调题的时间,我还写了一个这道题的易错点

  1. 当ai=aj,bi=bj,ci=cj时,就算i排在j前面,j也可以更新i,那么我们就需要处理一下这种特殊情况就行了,详见代码

t2

天使玩偶

之所以不把这道题数据加强版的名字写上去是因为我太弱了过不了那道题

前置芝士:cdq分治,树状数组

版本1:TLE版本(cdq套cdq)

//I love Nanami Chiaki
#include<bits/stdc++.h>
using namespace std;
const int maxn=500007;
const int maxp=1000007;
const int inf=1e9+7;
inline int read(){
    int re=0;char c=getchar();
    while (c<'0' || c>'9') c=getchar();
    while (c>='0' && c<='9'){re=(re<<1)+(re<<3)+(c^48);c=getchar();}
    return re;
}
struct Node{int x,y,t,op,f1;}hve[maxn<<1],hve1[maxn<<1],hve2[maxn<<1];
int n,m,mm=0,ans[maxn];
inline bool cmp(Node X,Node Y){return (X.x^Y.x)?(X.x<Y.x):((X.y^Y.y)?(X.y<Y.y):(X.t<Y.t));}
void solveit2(int l,int r){
    if (l==r) return;
    int mid=(l+r)>>1;
    solveit2(l,mid);
    solveit2(mid+1,r);
    int i=l,j=mid+1,sz=l,mx=-inf;
    while (i<=mid || j<=r){
        if (i>mid){
            if (hve1[j].op && hve1[j].f1) ans[hve1[j].op]=min(hve1[j].x+hve1[j].y-mx,ans[hve1[j].op]);
            hve2[sz++]=hve1[j++];
        }
        else if (j>r || hve1[i].t<hve1[j].t){
            if (!hve1[i].op && !hve1[i].f1) mx=max(mx,hve1[i].x+hve1[i].y);
            hve2[sz++]=hve1[i++];           
        }
        else{
            if (hve1[j].op && hve1[j].f1) ans[hve1[j].op]=min(hve1[j].x+hve1[j].y-mx,ans[hve1[j].op]);
            hve2[sz++]=hve1[j++];
        }
    }
    for (int i=l;i<=r;++i) hve1[i]=hve2[i];
}
void solveit1(int l,int r){
    if (l==r) return;
    int mid=(l+r)>>1;
    solveit1(l,mid);
    solveit1(mid+1,r);
    int i=l,j=mid+1,sz=i;
    while (i<=mid || j<=r){
        if (i>mid){hve1[sz++]=hve[j++];hve1[sz-1].f1=1;}
        else if (j>r || (hve[i].y<hve[j].y || (hve[i].y==hve[j].y && hve[i].t<hve[j].t))){hve1[sz++]=hve[i++];hve1[sz-1].f1=0;}
        else{hve1[sz++]=hve[j++];hve1[sz-1].f1=1;}
    }
    for (int i=l;i<=r;++i) hve[i]=hve1[i];
    solveit2(l,r);
}
int main(){
    n=read();m=read();
    for (int i=0;i<n;++i){int x=read(),y=read();hve[i].x=x;hve[i].y=y;hve[i].t=0;hve[i].op=0;}
    for (int j=0;j<m;++j){
        int T=read(),x=read(),y=read();hve[n+j].x=x;hve[n+j].y=y;hve[n+j].t=j+1;
        if (T==1) hve[n+j].op=0;
        else{hve[n+j].op=++mm;ans[mm]=inf;}
    }
    n+=m;m=mm;
    for (int k=0;k<4;++k){
        sort(hve,hve+n,cmp);
        solveit1(0,n-1);
        for (int j=0;j<n;++j){swap(hve[j].x,hve[j].y);hve[j].x=maxp-6-hve[j].x;}
    }
    for (int i=1;i<=m;++i) printf("%d\n",ans[i]);
    return 0;
}

版本2:AC版本(?)(cdq套树状数组)(还要在加上一些东西,就是以#pragma GCC optimize开头的一系列头文件才能过,手动尴尬)(为什么不放上去?太丑了!)

可看我的AC提交记录(之所以不用大号交是因为大号提交次数太多被限了,再次手动尴尬)

//I love Nanami Chiaki
#include<bits/stdc++.h>
using namespace std;
#define lowbit(x) x&(-x)
const int maxn=300007;
const int maxp=1000007;
const int inf=1e9+7;
inline int read(){
    int re=0;char c=getchar();
    while (!isdigit(c)) c=getchar();
    while (isdigit(c)){re=(re<<1)+(re<<3)+(c^48);c=getchar();}
    return re;
}
struct Node{int x,y,t,op;bool ff;}hve[maxn<<1],hve1[maxn<<1];
int n,m,mm=0,ans[maxn],dat[maxn];
inline bool cmp(Node X,Node Y){return (X.x^Y.x)?(X.x<Y.x):((X.y^Y.y)?(X.y<Y.y):(X.t<Y.t));}
inline void mymax(int &x,int y){if (x<y) x=y;}
inline void mymin(int &x,int y){if (x>y) x=y;}
inline void myswap(int &x,int &y){int t=x;x=y;y=t;}
inline void add(int p,int v){while (p<=m && dat[p]<v){mymax(dat[p],v);p+=lowbit(p);}}
inline int query(int p){int re=0;while (p){mymax(re,dat[p]);p-=lowbit(p);}return (re==0)?-inf:re;}
inline void clear(int p){while (p<=m && dat[p]){dat[p]=0;p+=lowbit(p);}} 
void solveit1(int l,int r){
    if (l==r) return;
    int mid=(l+r)>>1;
    solveit1(l,mid);
    solveit1(mid+1,r);
    int i=l,j=mid+1,sz=i;
    while (i<=mid || j<=r){
        if (i>mid){if (hve[j].op) mymin(ans[hve[j].op],hve[j].x+hve[j].y-query(hve[j].t));hve1[sz++]=hve[j++];}
        else if (j>r || (hve[i].y<hve[j].y || (hve[i].y==hve[j].y && hve[i].t<hve[j].t))){if (!hve[i].op) add(hve[i].t+1,hve[i].x+hve[i].y);hve1[sz++]=hve[i++];hve1[sz-1].ff=true;}
        else{if (hve[j].op) mymin(ans[hve[j].op],hve[j].x+hve[j].y-query(hve[j].t));hve1[sz++]=hve[j++];}
    }
    for (int i=l;i<=r;++i){
        if (hve1[i].ff){clear(hve1[i].t+1);hve1[i].ff=false;}
        hve[i]=hve1[i];
        ++i;if (i>r) break;
        if (hve1[i].ff){clear(hve1[i].t+1);hve1[i].ff=false;}
        hve[i]=hve1[i];
    }
}
int main(){
    n=read();m=read();
    int x,y,T;
    for (int i=0;i<n;++i){x=read();y=read();hve[i].x=x;hve[i].y=y;hve[i].t=0;hve[i].op=0;}
    for (int j=0;j<m;++j){
        T=read();x=read();y=read();hve[n+j].x=x;hve[n+j].y=y;hve[n+j].t=j+1;
        if (T==1) hve[n+j].op=0;
        else{hve[n+j].op=++mm;ans[mm]=inf;}
    }
    n+=m;
    for (int k=0;k<4;++k){
        sort(hve,hve+n,cmp);
        solveit1(0,n-1);
        for (int j=0;j<n;++j){
            myswap(hve[j].x,hve[j].y);hve[j].x=maxp-6-hve[j].x;
            ++j;if (j>=n) break;
            myswap(hve[j].x,hve[j].y);hve[j].x=maxp-6-hve[j].x;
        }
    }
    for (int i=1;i<=mm;++i) printf("%d\n",ans[i]);
    return 0;
}

这道题的大概思路:

当然我也是看题解的,一开始完全没觉得竟然是cdq的题,虽然是在cdq题表里面找的。

这道题的思路就是可以用给每个点和查询的点都设一个时间戳,然后对于每个查询的点,实际上我们就是去计算它左下角且时间戳在它前面的点的x+y的最大值就可以了,答案就是min(xi+xj-mx),这样当然是错的,还有左上,右上,右下等好几个方位,但这些方位本质上是和左下角相似地去求就行了(实际上把地图转一下就可以了),然后问题就被转化成了求四次对于每个查询的点i,只有x<=xi,y<=yi,t<ti的点才可以更新的问题,一看不就是上题吗

版本1:

我本来是想用这道题学一下cdq套cdq的,毕竟前面那题的解法没法解决四维偏序,结果成功在错误的题上使用了这个方法

不过还是讲一下吧

  1. cdq套cdq是这样子的
    • 第一步还是按照a排序
    • 然后进了在solve1中还是先分治,然后按照b排序去处理,但这时我们不考虑用其它数据结构维护。那么我们可以把在前一个子问题的数打一个下标0,后一个子问题的数打一个下标1,表示下标为0的数是可以用来更新下标1的数的答案的(在a属性的意义上)
    • 这时我们已经按照b属性排序好了,然后我们进入solve2
    • 在solve2中,我们还是要分治处理这个问题,我们知道这次分治是用来处理solve1中的两个子问题之间的答案的,所以我们只用下标为0的数去更新下标为1的数,分治处理,solve2的while循环处理的是solve2前一个子问题下标为0的数对solve2后一个子问题下标为1的数的答案更新(因为在solve2分治的两个子问题中,它们内部的这样的答案更新已经处理完了),那么按照c归并排序,由于solve2一开始的数组是按照b排序的,所以前一个子问题的数属性b一定小于后一个子问题属性b的情况下,在归并c的过程中,能在一个数之前更新mx的数,属性c一定也比它小。然后用用下标为0的数(在前一个子问题)去更新mx,mx更新下标为1的数(在后一个子问题)就没问题了
  2. 关于四维偏序的处理,大概是一样的,就是先把a变成有序的,然后把a变成0/1的情况下将b排成有序的,再然后将b变成0/1的情况下,将c排成有序的,最后更新答案就可以了

版本2:

卡常卡死人啊!!!,难受

我用/学到的几个卡常技巧

  1. 循环展开,新学的,感觉还蛮好的,不会的自行百度,我也是百度的

  2. 自定义max,min,swap

  3. 简单快读

  4. 就是前面那一堆头文件(帮我优化了一半的时间,手动尴尬)

待更

猜你喜欢

转载自www.cnblogs.com/xxjAc/p/12109935.html
今日推荐