zoukankan      html  css  js  c++  java
  • 「学习小结」CDQ 分治多维偏序问题

    一、分治基础

    1. 归并排序

    题目大意:给定一个长度为 n 的数组,你需要对它进行排序。1≤n≤105

    Solution:

    考虑把整个数组分成两个部分,等两部分都排完序后再把这两个有序的数组进行合并。动图演示

    合并的过程可以用双指针,时间复杂度是 O(n) 的。

    这样总时间复杂度 T(n)=2T(n/2)+O(n),得出 T(n)=O(n log n)。

    void solve(int l,int r){
        if(l==r) return ;
        int mid=(l+r)>>1;
        solve(l,mid),solve(mid+1,r);
        int x=l,y=mid+1,cnt=l;
        while(x<=mid&&y<=r){
            if(a[x]<a[y]) t[cnt++]=a[x++];
            else t[cnt++]=a[y++];
        }
        while(x<=mid) t[cnt++]=a[x++];
        while(y<=r) t[cnt++]=a[y++];
        for(int i=l;i<=r;i++) a[i]=t[i];
    }

    所谓分治,就是把一个规模为 n 的问题分解为两个规模减半的子问题,解决完子问题后再通过子问题的结果得到原问题的答案。

    2. 棋盘覆盖问题

    题目大意:有一个大小为 2n×2n 的棋盘,其中抠掉了一个方格。你需要用这种 L 形的骨牌去覆盖这张棋盘上其它剩余的部分。求一组解。1≤n≤10。

    Solution:

    我们把整个棋盘沿着中心线分成四个部分,这样被抠掉的那个格子一定位于其中一个部分里。

    在中心放一个 L,使得其刚好覆盖了其余三个部分各一个格子:

    然后就可以递归。由于不需要合并,时间复杂度和输出同阶。

    二、CDQ 分治

    有的时候,后面的值会依赖前面的值。比如 DP 的过程中,要求出后面的 DP 值,我们就需要先求出前面的 DP 值。

    这时候就需要用到 cdq 分治。这个分治的思路如下:

    1. 递归处理左边
    2. 计算左边对右边带来的贡献
    3. 递归处理右边

    即:在分治时考虑左边的区间内的元素对右边区间的元素的贡献。

    三、多维偏序问题

    1. 最长上升子序列(二维偏序)

    题目大意:有 n 个数 a1,...,an,求最长上升子序列的长度。1≤n≤105

    Solution:

    设 f[i] 表示以 i 结尾的最长上升子序列的长度。

    然后可以用 DP 来求解:f[i]=minj<i&aj<ai f[j]+1

    直接做时间复杂度是 O(n2) 的。

    考虑优化这个算法。

    设当前分治区间是 [l,r],我们递归求完了 [l,m] 的答案,接下来要处理这一段对 [m+1,r] 的贡献。

    把位于 [l,m] 的点看做插入,[m+1,r] 的点看做查询, 然后把整个区间按照 ai 排序。问题转化为支持插入 f[i] 和查询当前所有 f 的最大值。

    只需要记录一个前缀最大值就可以了。

    时间复杂度:O(n log n)。

    可以不用 cdq 分治。我们先把 ai 离散化,然后开一个树状数组。只需要实现单点修改和前缀查询最大值。时间复杂度还是 O(n log n)。

    2. 三维偏序

    题目大意:有 n 个元素,每一个元素有一个 ai 和 bi,求两个值都单调的最长上升子序列。1≤n≤105

    Solution:

    还是先考虑 n2 算法:f[i]=minj<i&aj<ai&bj<bi f[j]+1

    然后 cdq 分治。

    我们把 [l,r] 内的元素按照 ai 排序。接下来问题转化为支持插入一个 bi 以及询问所有满足 bj<bi 的 f[j] 的最大值。

    用树状数组解决。总时间复杂度 O(n log2 n)。

    3. 四维偏序

    题目大意:现在每一个值有三个属性 ai,bi,ci,还是求最长上升子序列。1≤n≤50000。

    先按照 ai 排序,cdq 分治后分治区间内转化为三维偏序问题。

    然后再 cdq 一次,内层用树状数组实现。

    时间复杂度:O(n log3 n)

    三、模板

    Luogu P3810「模板」三维偏序。

    题目大意:有 n 个元素,第 i 个元素有三个属性,设 f(i) 表示满足 aj≤ai 且 bj≤bi 且 cj≤ci 且 j≠i 的 j 的数量。对于 d∈[0,n),求 f(i)=d 的 i 数量。

    注:这道题给的是一个 n 个元素的集合,每个元素有三个属性,是无序的。而上面所写的三维偏序,这 n 个元素是有序的,所以下标也算一个属性。其他的同理。

    考虑先将第一维排序,然后分治。考虑只有左边的点会对右边的点有贡献,故在分治过程中考虑左边的区间内的点对右边区间内的点的贡献。按照第二维进行归并排序,当加入了一个左边的点时,假设其第三维大小为 x,相当于要加入一个元素 x,当加入了一个右边的点时,假设其第三维大小为 x,相当于要求加入的 ≤x 的元素有多少个,这个直接用树状数组维护即可。

    时间复杂度 O(n log2 n)。

    #include<bits/stdc++.h>
    #define int long long
    #define lowbit(x) x&(-x)
    using namespace std;
    const int N=2e5+5;
    int n,k,m,tot,c[N],t[N];
    struct data{
        int a,b,c,cnt,ans;    //cnt: 相同元素个数 
    }s1[N],s2[N];
    bool cmp1(data x,data y){    //按第一维排序
        if(x.a==y.a) return x.b==y.b?x.c<y.c:x.b<y.b;
        return x.a<y.a;
    }
    bool cmp2(data x,data y){    //按第二维排序
        return x.b==y.b?x.c<y.c:x.b<y.b;
    }
    void modify(int x,int y){    //树状数组的下标 i 表示的是权值为 i 的元素的个数 
        for(int i=x;i<=k;i+=lowbit(i))
            c[i]+=y;
    }
    int query(int x){
        int ans=0;
        for(int i=x;i;i-=lowbit(i))
            ans+=c[i];
        return ans;
    }
    void cdq(int l,int r){
        if(l==r) return ;
        int mid=(l+r)/2;
        cdq(l,mid),cdq(mid+1,r);    //类似归并排序 
        sort(s2+l,s2+1+mid,cmp2),sort(s2+mid+1,s2+1+r,cmp2);
        int x=mid+1,y=l;    //双指针。y 指向 [l,mid],x 指向 [mid+1,r]。对于每个 s2[i] 和 s2[j],此时已有 s2[i].a>=s2[j].a
        for(;x<=r;x++){
            while(s2[x].b>=s2[y].b&&y<=mid) modify(s2[y].c,s2[y].cnt),y++;    //若 s2[i].b>=s2[j].b并且 j<=mid,则将 s2[j] 加入到以 s2[j].c 为下标,s2[j].cnt 为权值的树状数组中
            s2[x].ans+=query(s2[x].c);    //保证树状数组里的数一定符合条件
        }
        for(int i=l;i<y;i++)
            modify(s2[i].c,-s2[i].cnt);    //清空树状数组
    }
    signed main(){
        scanf("%lld%lld",&n,&k);
        for(int i=1;i<=n;i++)
            scanf("%lld%lld%lld",&s1[i].a,&s1[i].b,&s1[i].c);
        sort(s1+1,s1+1+n,cmp1);    //以第一维为关键字排序
        for(int i=1;i<=n;i++){
            tot++;    //tot: 相同元素的个数 
            if(s1[i].a!=s1[i+1].a||s1[i].b!=s1[i+1].b||s1[i].c!=s1[i+1].c) s2[++m]=(data){s1[i].a,s1[i].b,s1[i].c,tot,0},tot=0;    //m: 不同元素的个数 
        }    //第一维已有序,合并元素
        cdq(1,m);
        for(int i=1;i<=m;i++)
            t[s2[i].ans+s2[i].cnt-1]+=s2[i].cnt;    //s2[i].ans+s2[i].cnt-1: 比 s2[i] 小的元素个数加上 s2[i] 的元素总数减 1 
        for(int i=0;i<n;i++)
            printf("%lld
    ",t[i]);
        return 0; 
    } 

    四、总结

    如果我们有 k 个限制,那么 cdq 一次之后就可以去掉其中的一个限制,其中时间复杂度多一个 log。

    不过 log4 及以上的复杂度就已经不如 O(n2) 算法快了,所以在实际应用中最多就是 cdq 套 cdq。

    绝大多数 cdq 分治的题目都可以转化为 k 维偏序(或者 k 维数点)。

  • 相关阅读:
    Codevs 4189 字典(字典树Trie)
    Codevs 1697 ⑨要写信
    Codevs 1904 最小路径覆盖问题
    特殊性
    继承
    分组选择符
    伪类选择符
    包含(后代)选择器
    子选择器
    类和ID选择器的区别
  • 原文地址:https://www.cnblogs.com/maoyiting/p/13382046.html
Copyright © 2011-2022 走看看