zoukankan      html  css  js  c++  java
  • 划分树---原理

    快速理解版本

    pku 2104

      给出n个数,m个询问,每次询问区间[x,y]中第k小的数是多少。
      求区间第k小,最简单的就是把数组排序,然后取下标为k的数,但m次排序后会超时,能否一次排序就解决m个询问呢?

      像快排一样,对于区间[l,r],取mid=(l+r)/2,把i<=mid的向左放,i>mid的向右放,然后递归处理,在排序的同时记录下排序的过程,询问时按记录的信息顺藤摸瓜就可以了,下面举个样例方便理解:


         在 [ 2 4 3 5 8 1 7 6 ]中找区间[2,7]的第2小
    ①           [ 2 4 3 5 8 1 7 6 ]            // 原数组  [2,7]找第2小
    ②     [ 2 4 3 1 ]         [ 5 8 7 6 ]      // 分成左右两组  知道[2,7]中有3个到左子树  故在答案左
    ③  [ 2 1 ]   [ 4 3 ]    [ 5 6 ]  [ 8 7 ]   // 同理  只有1个到左子树  故答案在右子树
    ④  [1] [2]   [3] [4]    [5] [6]  [7] [8]   // 一直迭代直到找到答案



    细节处理版本

    我们在求区间最值的时候,一般可以用线段树解决,但是如果要求区间第k小或者第k大值的话线段树就有点力不从心了,这是我们可以用划分树来解决。划分树利用了快速排序的思想,首先是建树,我们设当前区间的中位数为mid,(为了能快速找到区间的中位数,我们一般先对原序列做一次排序)则我们将区间中比mid小的放入左子树,将区间中比mid大数的放入右子树中,和mid相等的要讨论一下,有些需要放到左子树中,其他的放到右子树中,注意我们将数字放入子树的时候其相对顺序是不变的。这样我们一层一层下去,每次区间都减半,则空间消耗为O(nlogn)。下面看一个例子。

    假设序列长度为9,依次为 3 5 73 4 9 4 2 5,我们看看建树完成后是什么样子。

    sort[ ][2  3  3  4 4  5  5  7  9]

    tree[0][3  5  7  3 4  9  4  2 5]

    tree[1][3  3  4  4 2][5  7  9  5]

    tree[2][3  3  2][4  4][5 5][7  9]

    tree[3][3  2][3][4][4][5][5][7][9]

    tree[4][2][3][3][4][4][5][5][7][9]

    好了,树建完了,接下来就是最关键的查询了,我们设函数query(p,l,r,s,t,k)表示在第p层子树区间范围在[l,r]的子树中查找区间[s,t]中的第k小值。我们在每一颗子树中设sum[i]表示区间[l,i]范围内有多少个数字被放到了左子树中,那么我们容易得到,sum[t]-sum[s-1]表示在区间[s,t]有多少个树被放入了左子树中,我们不妨设这个值为num,若k<=num,我们就可以知道我们要找的数一定在左子树中,否则一定在右子树中,我们接下来只要继续往下遍历,当l==r的时候我们就可以确定我们要找的数,容易知道这一步骤的复杂度为O(log n),现在关键的一点就是如何再往下便利的时候确定s,t的值。这个其实自己画画图就很容易推出来的,下面只写结论,这里先设区间[l,s-1]中被放入左子树的数有snum个(sum[s-1]),当前区间的中点为mid,

    则若k<=num,我们返回(注意扣掉1)query(p+1,l,mid,l+snum,l+sum[t]-1,k),(要找的数在左子树上)

    否则,我们返回query(p+1,mid+1,r,mid+1+(s-l)-snum,mid+(t-(l-1))-sum[t],k-num);(要找的数在右子树上) 

     为排除掉左边的;注意理解(s-l)以及(t-(l-1))的意义:

    下面再举个例子,数和上面一样

    我们现在要找到区间[2,7]中的第3小数。

    sort[ ][2  3  3  4 4  5  5  7  9]

    tree[0][3  5 7  3  4  9  4 2  5]

    tree[1][3  3 4  4  2][5  7  9 5]

    tree[2][3  3  2][4  4][5  5][7  9]

    tree[3][3  2][3][4][4][5][5][7][9]

    tree[4][2][3][3][4][4][5][5][7][9]

    以上橙黄色背景的数字即为我们要求的范围。

    我们首先在第一层树上寻找,即query(0,1,9,2,7,3),我们发现在[2,7]区间中有3个数被放入了左子树,满足k<=num,所以我们往左子树中找,调用query(1,1,5,2,4,3)。

    在第二层树中我们发现区间[2,4]只有一个数被放入左子树,所以我们要找的数一定在右子树中,调用query(2,4,5,4,5,2)。

    在第三层树中,我们发现区间[4,5]只有一个数被放入左子树,同理我们应该往右子树找,调用query(3,5,5,5,5,1)。现在可以发现l==r,则我们可以确定已经找到了要找的数,则返回4即可,我们可知在区间[2,7]上第3小的数为4。

    划分树模板:

    1.0版(感觉还能优化)

    #include <iostream>
    #include <string.h>
    #include <stdio.h>
    #include <algorithm>
    #define maxn 100010
    #define mid ((l+r)>>1)
    using namespace std;
    int t[20][maxn],sum[20][maxn];//20层每层maxn t用来放原序; sum[p][i]表示第P层第i个放左节点的元素个数
    int as[maxn];//我们要输入的原始数组赋给t[0][]; 之后进行sort
    //以下为查找区间第k小划分树
    void build(int p,int l,int r) //p:第几层 默认0开始  ; l,r 左右区间从[1,n]开始建
    {
        int lm=0,i,ls=l,rs=mid+1;//lm表示应被放入左子树且与中位数相等的数有多少个,ls为左子树的起始位置,rs为右子树的起始位置
        for(i=mid;i>=l;i--) //求lm ;2 3 3 4 4 5 5 7 9得到的lm=2
        {
            if(as[i]==as[mid])
            lm++;
            else
            break;
        }
        for(i=l;i<=r;i++)
        {
            if(i==l)//这里要特殊讨论
            sum[p][i]=0;
            else
            sum[p][i]=sum[p][i-1];//下一个肯定是上一个+0或1
            if(t[p][i]==as[mid])//若与中位数相等则判断是否应该被放入左子树
            {
                if(lm)
                {
                    lm--;
                    sum[p][i]++;  //如果满足 说明又多了一个元素放左节点了
                    t[p+1][ls++]=t[p][i];//放入下一个t[]
                }
                else
                t[p+1][rs++]=t[p][i];
            }
            else if(t[p][i]<as[mid])//查找区间第K大即为>
            {
                sum[p][i]++;
                t[p+1][ls++]=t[p][i];
            }
            else
            t[p+1][rs++]=t[p][i];
        }
        if(l==r)
        return;
        build(p+1,l,mid);
        build(p+1,mid+1,r);
    }
    int query(int p,int l,int r,int ql,int qr,int k)
    {
        int s,ss;//s表示[l,ql-1]放入左子树的个数,ss表示区间[ql,qr]被放入左子树的个数
        if(l==r)//找到所求的数
        return t[p][l];
        if(ql==l)
        s=0,ss=sum[p][qr];
        else
        s=sum[p][ql-1],ss=sum[p][qr]-s;
        if(k<=ss)//要找的数在左子树中
        return query(p+1,l,mid,l+s,l+sum[p][qr]-1,k);
        else//要找的数在右子树中
        return query(p+1,mid+1,r,mid+1-l+ql-s,mid+1-l+qr-sum[p][qr],k-ss);
    }
    int main()
    {
       int i,n,m;
       scanf("%d%d",&n,&m);
       for(i=1;i<=n;i++)
       {
           scanf("%d",&as[i]);
           t[0][i]=as[i];
       }
       sort(as+1,as+n+1);
       build(0,1,n);
       while(m--)
       {
           int l,r,k;
           scanf("%d%d%d",&l,&r,&k);
           int ans=query(0,1,n,l,r,k);
           printf("%d
    ",ans);
       }
        return 0;
    }
    /*
     *
     input:
     7 3
    1 5 2 6 3 7 4
    2 5 3
    4 4 1
    1 7 3
    output:
    5
    6
    3
     * */
    


     1.1版 对变量名称进行了修改看上去比较易懂,部分地方的优化 给出更完善的注释 避免读者不知道为什么突然这一步出现的理由是什么


    #include <iostream>
    #include <string.h>
    #include <algorithm>
    #define maxn 100010
    #define mid ((L+R)>>1)
    using namespace std;
    int tree[20][maxn];//表示每层每个位置的值
    int toleft[20][maxn];//20层每层maxn t用来放原序; toleft[p][i]表示第P层第i个放左节点的元素个数
    int sorted[maxn];//已经排序的数
    //以下为查找区间第k小划分树
    void build(int p,int L,int R) //p:第几层 默认0开始  ; L,R 左右区间从[1,n]开始建
    {
    	if(L==R) return;  //这个放最上边省时
        int lm=0,i,ls=L,rs=mid+1;//lm表示应被放入左子树且与中位数相等的数有多少个,ls为左子树的起始位置,rs为右子树的起始位置
        for(i=mid;i>=L;i--) //求lm ;2 3 3 4 4 5 5 7 9得到的lm=2
        {           
            if(sorted[i]==sorted[mid])
            lm++;          //之前有一个错误的想法: 不记录这个lm  找的时候直接找<=的  但是这样会出错  比如 2 4 4 4 5 1   
            else         // 排序: 1 2 4 4 4 5   那如果我们直接找<=的(6/2=3)个的话 会找到 2 4 4这样就错了所以还是要记录lm
            break;
        }
        for(i=L;i<=R;i++)
        {
            if(i==L)//这里要特殊讨论(原因:间接的对所有初始化 我们这样做便于toleft[p][i]=toleft[p][i-1]这一部的处理)
            toleft[p][i]=0;     //
            else
            toleft[p][i]=toleft[p][i-1];//下一个肯定是上一个+0或1
            
            if(tree[p][i]==sorted[mid])//若与中位数相等则判断是否应该被放入左子树
            {
                if(lm)
                {
                    lm--;
                    toleft[p][i]++;  //如果满足 说明又多了一个元素放左节点了
                    tree[p+1][ls++]=tree[p][i];//放入下一个t[]
                }
                else
                tree[p+1][rs++]=tree[p][i];
            }
            else if(tree[p][i]<sorted[mid])//查找区间第K大即为>
            {
                toleft[p][i]++;
                tree[p+1][ls++]=tree[p][i];
            }
            else
            tree[p+1][rs++]=tree[p][i];
        }
        build(p+1,L,mid);
        build(p+1,mid+1,R);
    }
    //查询区间第k大的数,[L,R]是大区间,[l,r]是要查询的小区间
    int query(int p,int L,int R,int l,int r,int k)
    {
        int s,ss;//s表示[L,l-1]放入左子树的个数,ss表示区间[l,r]被放入左子树的个数
        if(L==R)//找到所求的数
        return tree[p][L];
        if(l==L)
        s=0,ss=toleft[p][r];
        else
        s=toleft[p][l-1],ss=toleft[p][r]-s;
        if(k<=ss)//要找的数在左子树中
        return query(p+1,L,mid,L+s,L+toleft[p][r]-1,k);
        else//要找的数在右子树中
        return query(p+1,mid+1,R,mid+1-L+l-s,mid+1-L+r-toleft[p][r],k-ss);
    }
    int main()
    {
       int i,n,m;
       cin>>n>>m;
       for(i=1;i<=n;i++)
       {
           cin>>tree[0][i];
           sorted[i]=tree[0][i];
       }
       sort(sorted+1,sorted+n+1);
       build(0,1,n);
       while(m--)
       {
           int l,r,k;
           cin>>l>>r>>k;
           int ans=query(0,1,n,l,r,k);
           cout<<ans<<endl;
       }
        return 0;
    }
    /*
     *
     input:
    7 3
    1 5 2 6 3 7 4
    2 5 3
    4 4 1
    1 7 3
    output:
    5
    6
    3
     * */
    



    版权声明:本文为博主原创文章,未经博主允许不得转载。

    today lazy . tomorrow die .
  • 相关阅读:
    SpringMVC在使用Jackson2时关于日期类型格式化的问题
    Redis入门到高可用(八)——list
    LongAdder,AtomicIntegerFieldUpdater深入研究
    Redis入门到高可用(七)——Hash
    CAS缺点
    MySQL 当记录不存在时插入(insert if not exists)
    Redis入门到高可用(六)—— 字符串
    jsonp 跨域
    jvm
    指令重排序
  • 原文地址:https://www.cnblogs.com/france/p/4808710.html
Copyright © 2011-2022 走看看