• 划分树讲解


      划分树,类似线段树,主要用于求解某个区间的第k 大元素(时间复杂度log(n)),快排本也可以快速找出,但快排会改变原序列,所以每求一次都得恢复序列。

      下面就以 POJ 2104 进行解说:

      题目意思就是,给你n 个数的原序列,有m 次询问,每次询问给出l、r、k,求原序列l 到r 之间第k 大的数。n范围10万,m范围5千,这道题用快排也可以过,快排过的时间复杂度n*m,而划分树是m*logn(实际上应该是nlogn才对,因为建图时间是nlogn,n又比m大),分别AC后,时间相差很明显。

      划分树,顾名思义是将n 个数的序列不断划分,根结点就是原序列,左孩子保存父结点所有元素排序后的一半,右孩子也存一半,也就是说排名1 -> mid的存在左边,排名(mid+1) -> r 的存在右边,同一结点上每个元素保持原序列中相对的顺序。见下图:

       

      点标记的就是进入左孩子的元素。

      当然,一般不会说每个结点开个数组存数,经观察,每一层都包含原本的n 个数,只是顺序不同而已,所以我们可以开val[20][N]来保存,也就是说共20层,每一层N个数。

      我们还需要一个辅助数组num,num[i]表示i 前面有多少数进入左孩子(i 和i 前面可以弄成本结点内也可以是所有,两种风格不同而已,下面采取的是本结点内),和val一样,num也开成num[20][N],来表示每一层,i 和i 前面(本结点)有多少进入左孩子。

      第一层:1 进入左孩子,num[1]=1,5 进入右孩子,num[2]=1,...,num[8]=4。

      第二层:5 进入左孩子,num[5]=1,6 进入右孩子,num[6]=1,...,num[8]=2。

      建图时就是维护每一层val[]和num[]的值就可以了。

     1 int a[N];       //原数组
     2 int sorted[N];  //排序好的数组
     3 //是一棵树,但把同一层的放在一个数组里。
     4 int num[20][N];   //num[i] 表示i前面有多少个点进入左孩子
     5 int val[20][N];   //20层,每一层元素排放,0层就是原数组
     6 void build(int l,int r,int ceng)
     7 {
     8   if(l==r) return ;
     9   int mid=(l+r)/2,isame=mid-l+1;  //isame保存有多少和sorted[mid]一样大的数进入左孩子
    10   for(int i=l;i<=r;i++) if(val[ceng][i]<sorted[mid]) isame--;
    11   int ln=l,rn=mid+1;   //本结点两个孩子结点的开头,ln左
    12   for(int i=l;i<=r;i++)
    13   {
    14     if(i==l) num[ceng][i]=0;
    15     else num[ceng][i]=num[ceng][i-1];
    16     if(val[ceng][i]<sorted[mid] || val[ceng][i]==sorted[mid]&&isame>0)
    17     {
    18       val[ceng+1][ln++]=val[ceng][i];
    19       num[ceng][i]++;
    20       if(val[ceng][i]==sorted[mid]) isame--;
    21     }
    22     else
    23     {
    24       val[ceng+1][rn++]=val[ceng][i];
    25 }
    26   }
    27   build(l,mid,ceng+1);
    28   build(mid+1,r,ceng+1);
    29 }

      查询时,比如要查找2 到6 之间第3 大的数,那么先判断2 到6 之间有多少元素进入左子树,(在此忽略细节)num[6]-num[2-1]=2,就说明2 到6 有两个数进入左子树,又因为我们要找的是第3 大的数,所以一定在右子树中。可以算出,下标2 前面有0 个数进入右子树,所以2 到6 之间进入右子树的元素在下一层一定是从5 开始排的,2 到6 的区间进入右子树3 个,所以下一层从5 排到7 都是原本2 到6 之间的。现在,因为去左子树两个,所以现在要在右子树找5 到7 之间排名第1 的元素。在下一层的查找步骤和第一层一样,也就是不断递归,当跑到叶子结点时就可以返回正确的值了。

     1 int look(int ceng,int sl,int sr,int l,int r,int k)
     2 {
     3   if(sl==sr) return val[ceng][sl];
     4   int ly;  //ly 表示l 前面有多少元素进入左孩子
     5   if(l==sl) ly=0;  //和左端点重合时
     6   else ly=num[ceng][l-1];
     7   int tolef=num[ceng][r]-ly;  //这一层l到r之间进入左子树的有tolef个
     8   if(tolef>=k)
     9   {
    10     return look(ceng+1,sl,(sl+sr)/2,sl+ly,sl+num[ceng][r]-1,k);
    11   }
    12   else
    13   {
    14     // l-sl 表示l前面有多少数,再减ly 表示这些数中去右子树的有多少个
    15     int lr = (sl+sr)/2 + 1 + (l-sl-ly);  //l-r 去右边的开头位置
    16     // r-l+1 表示l到r有多少数,减去去左边的,剩下是去右边的,去右边1个,下标就是lr,所以减1
    17     return look(ceng+1,(sl+sr)/2+1,sr,lr,lr+r-l+1-tolef-1,k-tolef);
    18   }
    19 }

      上述的查询采取了二分的思路,所以时间复杂度logn。建图时,每一层n 个元素,共有logn 层,所以时间复杂度是nlogn。

      上题AC代码:

      

     1 #include<stdio.h>
     2 #include<string.h>
     3 #include<algorithm>
     4 #define N 100010
     5 using namespace std;
     6 typedef long long LL;
     7 int a[N];       //原数组
     8 int sorted[N];  //排序好的数组
     9 //是一棵树,但把同一层的放在一个数组里。
    10 int num[20][N];   //num[i] 表示i前面有多少个点进入左孩子
    11 int val[20][N];   //20层,每一层元素排放,0层就是原数组
    12 void build(int l,int r,int ceng)
    13 {
    14   if(l==r) return ;
    15   int mid=(l+r)/2,isame=mid-l+1;  //isame保存有多少和sorted[mid]一样大的数进入左孩子
    16   for(int i=l;i<=r;i++) if(val[ceng][i]<sorted[mid]) isame--;
    17   int ln=l,rn=mid+1;   //本结点两个孩子结点的开头,ln左
    18   for(int i=l;i<=r;i++)
    19   {
    20     if(i==l) num[ceng][i]=0;
    21     else num[ceng][i]=num[ceng][i-1];
    22     if(val[ceng][i]<sorted[mid] || val[ceng][i]==sorted[mid]&&isame>0)
    23     {
    24       val[ceng+1][ln++]=val[ceng][i];
    25       num[ceng][i]++;
    26       if(val[ceng][i]==sorted[mid]) isame--;
    27     }
    28     else
    29     {
    30       val[ceng+1][rn++]=val[ceng][i];
    31 }
    32   }
    33   build(l,mid,ceng+1);
    34   build(mid+1,r,ceng+1);
    35 }
    36 
    37 
    38 int look(int ceng,int sl,int sr,int l,int r,int k)
    39 {
    40   if(sl==sr) return val[ceng][sl];
    41   int ly;  //ly 表示l 前面有多少元素进入左孩子
    42   if(l==sl) ly=0;  //和左端点重合时
    43   else ly=num[ceng][l-1];
    44   int tolef=num[ceng][r]-ly;  //这一层l到r之间进入左子树的有tolef个
    45   if(tolef>=k)
    46   {
    47     return look(ceng+1,sl,(sl+sr)/2,sl+ly,sl+num[ceng][r]-1,k);
    48   }
    49   else
    50   {
    51     // l-sl 表示l前面有多少数,再减ly 表示这些数中去右子树的有多少个
    52     int lr = (sl+sr)/2 + 1 + (l-sl-ly);  //l-r 去右边的开头位置
    53     // r-l+1 表示l到r有多少数,减去去左边的,剩下是去右边的,去右边1个,下标就是lr,所以减1
    54     return look(ceng+1,(sl+sr)/2+1,sr,lr,lr+r-l+1-tolef-1,k-tolef);
    55   }
    56 }
    57 
    58 int main()
    59 {
    60   int n,m,l,r,k;
    61   while(scanf("%d%d",&n,&m)!=EOF)
    62   {
    63 
    64     for(int i=1;i<=n;i++)
    65     {
    66       scanf("%d",&val[0][i]);
    67       sorted[i]=val[0][i];
    68     }
    69     sort(sorted+1,sorted+n+1);
    70     build(1,n,0);
    71     while(m--)
    72     {
    73       scanf("%d%d%d",&l,&r,&k);
    74       printf("%d
    ",look(0,1,n,l,r,k));
    75     }
    76   }
    77   return 0;
    78 }
  • 相关阅读:
    移动前端工作的那些事---前端制作之动画效率问题简析
    PHP从零开始-笔记-面向对象编程的概念
    php从零开始
    jquery表单验证
    Jquery网页加载进度条(随笔,当然要随便写,当日记动态心情写咯)
    Jquery实现花瓣随机飘落(收藏自慕课网)
    seajs的那点事(很坑的事),和本白的一点事(更坑的事)
    js高级群的一些整理6月
    有关jquery checkbox获取checked的问题
    最近忙着考试又是什么的,然后群里都在秀战绩,秀一下那些年的战绩吧
  • 原文地址:https://www.cnblogs.com/hchlqlz-oj-mrj/p/5744308.html
Copyright © 2020-2023  润新知