• CDQ分治入门


    前言

    \(CDQ\)分治是一个神奇的算法。

    它有着广泛的用途,甚至在某些题目中还能取代\(KD-Tree\)树套树等恶心的数据结构成为正解,而且常数还小得多。

    不过它也有一定的缺点,如必须离线操作,遇到强制在线的题目还是老老实实打树套树吧... ...

    核心思想

    \(CDQ\)分治的核心思想真的是非常简单,也就是二字(其实所有分治算法都是这样)。

    • 分: 与常见的二分一样,将\([l,r]\)区间内的问题分成两个区间\([l,mid]\)\([mid+1,r]\)解决。
    • 治: \(CDQ\)分治中的这一部分就十分玄学了,它的思想是利用左区间求解右区间,这与普通的分治就大不一样了。

    这样讲毕竟还是十分抽象,让我们来借助一道经典例题,来粗略地见识一下\(CDQ\)分治的神奇所在。

    经典例题:【BZOJ3262】陌上花开

    这道题目大致题意就是要你求三维偏序

    关于二维偏序

    谈到三维偏序,我们可能首先会想到二维偏序

    或许有些人不知道什么是二维偏序,但它的另一个名称——逆序对你总知道吧。

    二维偏序一般可以用树状数组归并排序来解决。

    关于用树状数组,其实我们接下来还要用到。

    而对于归并排序,可以发现它其实也是借助了左区间来求解右区间,或许也能算作一个比较\(Simple\)\(CDQ\)分治?(大雾)

    好了,关于逆序对我们就扯到这里,下面我们来看看如何用\(CDQ\)分治求解三维偏序。

    如何求解三维偏序

    • 对于第一维
      • 首先第一步是将数据按照第一维\(x\)进行排序。
      • 这样就能保证第一维是有序的了。
    • 对于第二维
      • 接下来,在每一次处理完两个子区间的答案后(注意,一定要先处理子区间,因为接下来的排序会打乱元素的顺序),我们再将这两个子区间分别按照第二维\(y\)排序。
      • 此时,我们依然可以保证,左区间内每个元素的第一维始终小于右区间内每个元素的第一维。(这应该是显然的吧)
    • 对于第三维
      • 我们可以用\(i\)\(j\)分别记录右区间左区间当前处理到的点。
      • 对于每一个\(y_j\le y_i\)\(j\),我们可以将其第三维\(z\)加入树状数组
      • 由于两个区间经过排序,\(y\)的大小是递增的,所以\(j\)的大小也是递增的,这样就能稳定时间复杂度。
      • 现在,我们已经保证在树状数组中的所有元素,它的前两维皆\(\le\)当前\(i\)的前两维。因此,我们只要求出有多少个\(z\le z_i\)即可,用树状数组可以快速做到这一点。
      • 这样一来,就能计算出\(i\)的三维偏序数量了。

    大致就是这样一个过程,一次没有看明白的可以再多看几遍理解一下。

    最后注意一个细节:千万记得清空树状数组!而且千万记得不要直接\(memset\)

    代码

    #include<bits/stdc++.h>
    #define max(x,y) ((x)>(y)?(x):(y))
    #define min(x,y) ((x)<(y)?(x):(y))
    #define uint unsigned int
    #define LL long long
    #define ull unsigned long long
    #define swap(x,y) (x^=y,y^=x,x^=y)
    #define abs(x) ((x)<0?-(x):(x))
    #define INF 1e9
    #define Inc(x,y) ((x+=(y))>=MOD&&(x-=MOD))
    #define ten(x) (((x)<<3)+((x)<<1))
    #define N 100000
    using namespace std;
    int n,m,nn;
    struct value
    {
        int x,y,z,v,tot;
        inline friend bool operator == (value x,value y) {return !(x.x^y.x||x.y^y.y||x.z^y.z);}
    }s[N+5];
    class FIO
    {
        private:
            #define Fsize 100000
            #define tc() (FinNow==FinEnd&&(FinEnd=(FinNow=Fin)+fread(Fin,1,Fsize,stdin),FinNow==FinEnd)?EOF:*FinNow++)
            #define pc(ch) (FoutSize<Fsize?Fout[FoutSize++]=ch:(fwrite(Fout,1,FoutSize,stdout),Fout[(FoutSize=0)++]=ch))
            int f,FoutSize,OutputTop;char ch,Fin[Fsize],*FinNow,*FinEnd,Fout[Fsize],OutputStack[Fsize];
        public:
            FIO() {FinNow=FinEnd=Fin;}
            inline void read(int &x) {x=0,f=1;while(!isdigit(ch=tc())) f=ch^'-'?1:-1;while(x=ten(x)+(ch&15),isdigit(ch=tc()));x*=f;}
            inline void read_char(char &x) {while(isspace(x=tc()));}
            inline void read_string(string &x) {x="";while(isspace(ch=tc()));while(x+=ch,!isspace(ch=tc())) if(!~ch) return;}
            inline void write(int x) {if(!x) return (void)pc('0');if(x<0) pc('-'),x=-x;while(x) OutputStack[++OutputTop]=x%10+48,x/=10;while(OutputTop) pc(OutputStack[OutputTop]),--OutputTop;}
            inline void write_char(char x) {pc(x);}
            inline void write_string(string x) {register int i,len=x.length();for(i=0;i<len;++i) pc(x[i]);}
            inline void end() {fwrite(Fout,1,FoutSize,stdout);}
    }F;
    inline bool cmp_x(value x,value y) {return x.x^y.x?x.x<y.x:(x.y^y.y?x.y<y.y:x.z<y.z);}//按第一维排序
    inline bool cmp_y(value x,value y) {return x.y^y.y?x.y<y.y:x.z<y.z;}//按第二维排序
    class Class_CDQ//CDQ分治
    {
        private:
            int ans[N+5];//最后统计答案
            class Class_BIT//树状数组
            {
                private:
                    #define M 200000
                    #define lowbit(x) ((x)&-(x))
                    int data[M+5];
                public:
                    inline void Add(int x,int v) {while(x<=m) data[x]+=v,x+=lowbit(x);}//插入元素
                    inline int Query(int x,int ans=0) {while(x) ans+=data[x],x-=lowbit(x);return ans;}//询问≤x的数的和
            }BIT;
        public:
            inline void Solve(int l,int r)//求解l到r这段区间内的答案
            {
                if(l>=r) return;
                register int mid=l+r>>1,i,j=l;
                Solve(l,mid),Solve(mid+1,r),sort(s+l,s+mid+1,cmp_y),sort(s+mid+1,s+r+1,cmp_y);//切记先求解子区间,然后再排序,排序之后依然能保证右区间第一维大于左区间第一维
                for(i=mid+1;i<=r;++i)
                {
                    while(j<=mid&&s[j].y<=s[i].y) BIT.Add(s[j].z,s[j].v),++j;//对于每一个y[j]≤y[i]的j,将z[j]插入树状数组
                    s[i].tot+=BIT.Query(s[i].z);//求出树状数组中≤z[i]的所有元素之和,从而更新i的三维偏序个数
                }
                for(i=l;i<j;++i) BIT.Add(s[i].z,-s[i].v);//切记要这样清空树状数组,memset会T飞(亲身实践)
            }
            inline void PrintAns()
            {
                register int i;
                for(i=1;i<=n;++i) ans[s[i].tot+s[i].v-1]+=s[i].v;//统计答案
                for(i=0;i<nn;++i) F.write(ans[i]),F.write_char('\n');//输出
            }
    }CDQ;
    int main()
    {
        register int i;
        for(F.read(nn),F.read(m),i=1;i<=nn;++i) F.read(s[i].x),F.read(s[i].y),F.read(s[i].z),s[i].v=1;
        for(sort(s+1,s+nn+1,cmp_x),i=1;i<=nn;++i) n&&s[n]==s[i]?++s[n].v:(s[++n]=s[i],0);//按照第一维排序,然后去重,从而提高时间效率
        return CDQ.Solve(1,n),CDQ.PrintAns(),F.end(),0;//用CDQ分治求解
    }
    
    

    后记

    关于\(CDQ\)分治求解三维偏序,还有一道比较好的题目:【洛谷3157】[CQOI2011] 动态逆序对,可以去做一做。
    (这道题卡树套树,我的 线段树套\(Treap\) 只得了\(80\)分,于是为做这题来学了\(CDQ\)分治)

    败得义无反顾,弱得一无是处
  • 相关阅读:
    SharePoint 2016 图文安装教程
    CSS选择器的特殊性和LOVE HA
    CSS相邻兄弟选择器
    Javascript高级程序设计——函数
    Javascript高级程序设计——基本概念(二)
    Javascript高级程序设计——基本概念(一)
    Javascript高级程序设计——在HTML中使用Javascript
    Javascript高级程序设计——javascript简介
    Javascript包含对象的数组去重
    JQuery阻止表单提交的方法总结
  • 原文地址:https://www.cnblogs.com/chenxiaoran666/p/CDQ.html
Copyright © 2020-2023  润新知