1 条题解

  • 0
    @ 2025-8-24 21:52:59

    自动搬运

    查看原文

    来自洛谷,原作者为

    avatar FlashHu
    **

    搬运于2025-08-24 21:52:59,当前版本为作者最后更新于2018-07-28 10:38:41,作者可能在搬运后再次修改,您可在原文处查看最新版

    自动搬运只会搬运当前题目点赞数最高的题解,您可前往洛谷题解查看更多

    以下是正文


    安利蒟蒻CDQ分治总结

    分治就是分治,“分而治之”的思想。

    那为什么会有CDQ分治这样的称呼呢?

    这一类分治有一个重要的思想——用一个子问题来计算对另一个子问题的贡献。

    有了这种思想,就可以方便地解决更复杂的问题。

    这样一句话怎样理解好呢?还是做做题目吧。

    三维偏序问题,即给出若干元素,每个元素有三个属性值a,b,ca,b,c,询问对于每个元素ii,满足ajai,bjbi,cjcia_j\leq a_i,b_j\leq b_i,c_j\leq c_ijj的个数

    不用着急,先从简单的问题开始

    试想一下二位偏序也就是ajai,bjbia_j\leq a_i,b_j\leq b_i怎么做

    先按aa为第一关键字,bb为第二关键字排序,那么我们就保证了第一维aa的有序。

    于是,对于每一个ii,只可能11i1i-1的元素会对它有贡献,那么直接查11i1i-1的元素中满足bjbib_j\leq b_i的元素个数。

    具体实现?动态维护bb的树状数组,从前到后扫一遍好啦,O(nlogn)O(n\log n)

    那么三维偏序呢?我们只有在保证前两位都满足的情况下才能计算答案了。

    仍然按aa为第一关键字,bb为第二关键字,cc为第三关键字排序,第一维保证左边小于等于右边了。

    为了保证第二维也是左边小于等于右边,我们还需要排序。

    想到归并排序是一个分治的过程,我们可不可以在归并的过程中,统计出在子问题中产生的对答案贡献呢?

    现在我们有一个序列,我们把它递归分成两个子问题,子问题进行完归并排序,已经保证bb有序。此时,两个子问题间有一个分界线,原来第一维左边小于等于右边,所以现在分界线左边的任意一个的aa当然还是都小于右边的任意一个。那不等于说,只有分界线左边的能对右边的产生贡献?

    于是,问题降到了二维。我们就可以排序了,归并排序(左边的指针为jj,右边的为ii)并维护cc的树状数组,如果当前bjbib_j\leq b_i,说明jj可以对后面加入的满足cjcic_j\leq c_iii产生贡献了,把cjc_j加入树状数组;否则,因为后面加入的jj都不会对ii产生贡献了,所以就要统计之前被给的所有贡献了,查询树状数组cic_i的前缀和。

    这是在分治中统计的子问题的答案,跟总答案有怎样的关系呢?容易发现,每个子问题统计的只有跨越分界线的贡献,反过来看,每一个能产生贡献的i,ji,j,有且仅有一个子问题,两者既同时被包含,又在分界线的异侧。那么所有子问题的贡献加起来就是总答案。

    算法的大致思路就是这样啦。至于复杂度,T(n)=O(nlogk)+2T(2n)=O(nlognlogk)T(n)=O(n\log k)+2T(\frac 2 n)=O(n\log n\log k)

    当然还有不少细节问题。

    最大的问题就在于,可能有完全相同的元素。这样的话,本来它们相互之间都有贡献,可是cdq的过程中只有左边的能贡献右边的。这可怎么办呢?

    我们把序列去重,这样现在就没有相同的了。给现在的每个元素一个权值vv等于出现的次数。中间的具体实现过程也稍有变化,在树状数组中插入的值是vv而不是11了,最后统计答案时,也要算上相同元素内部的贡献,ans+=v-1

    写法上,为了防止sort和归并排序中空间移动太频繁,没有对每个元素封struct,这样的话就要膜改一下cmp函数(蒟蒻也是第一次发现cmp可以这么写)

    蒟蒻还是觉得开区间好写一些吧。。。当然闭区间好理解些。。。

    #include<cstdio>
    #include<cstring>
    #include<algorithm>
    #define RG register
    #define R RG int
    using namespace std;
    const int N=1e5+9,SZ=2.2e6;
    char buf[SZ],*pp=buf-1;//fread必备
    int k,a[N],b[N],c[N],p[N],q[N],v[N],cnt[N],ans[N],*e;
    inline int in(){
        while(*++pp<'-');
        R x=*pp&15;
        while(*++pp>'-')x=x*10+(*pp&15);
        return x;
    }
    void out(R x){
        if(x>9)out(x/10);
        *++pp=x%10|'0';
    }
    inline bool cmp(R x,R y){//直接对数组排序,注意三关键字
        return a[x]<a[y]||(a[x]==a[y]&&(b[x]<b[y]||(b[x]==b[y]&&c[x]<c[y])));
    }
    inline void upd(R i,R v){//树状数组修改
        for(;i<=k;i+=i&-i)e[i]+=v;
    }
    inline int ask(R i){//树状数组查前缀和
        R v=0;
        for(;i;i-=i&-i)v+=e[i];
        return v;
    }
    void cdq(R*p,R n){//处理一个长度为n的子问题
        if(n==1)return;
        R m=n>>1,i,j,k;
        cdq(p,m);cdq(p+m,n-m);//递归处理
        memcpy(q,p,n<<2);//归并排序
        for(k=i=0,j=m;i<m&&j<n;++k){
            R x=q[i],y=q[j];
            if(b[x]<=b[y])upd(c[p[k]=x],v[x]),++i;//左边小,插入
            else  cnt[y]+=ask(c[p[k]=y])     ,++j;//右边小,算贡献
        }
        for(;j<n;++j)cnt[q[j]]+=ask(c[q[j]]);//注意此时可能没有完成统计
        memcpy(p+k,q+i,(m-i)<<2);
        for(--i;~i;--i)upd(c[q[i]],-v[q[i]]);//必须这样还原树状数组,memset是O(n^2)的
    }
    int main(){
        fread(buf,1,SZ,stdin);
        R n=in(),i,j;k=in();e=new int[k+9];
        for(i=0;i<n;++i)
            p[i]=i,a[i]=in(),b[i]=in(),c[i]=in();
        sort(p,p+n,cmp);
        for(i=1,j=0;i<n;++i){
            R x=p[i],y=p[j];++v[y];//模仿unique双指针去重,统计v
            if(a[x]^a[y]||b[x]^b[y]||c[x]^c[y])p[++j]=x;
        }
        ++v[p[j++]];
        cdq(p,j);
        for(i=0;i<j;++i)
            ans[cnt[p[i]]+v[p[i]]-1]+=v[p[i]];//答案算好
        for(pp=buf-1,i=0;i<n;++i)
            out(ans[i]),*++pp='\n';
        fwrite(buf,1,pp-buf+1,stdout);
    }
    
    • 1

    信息

    ID
    2768
    时间
    1000ms
    内存
    500MiB
    难度
    6
    标签
    递交数
    0
    已通过
    0
    上传者