zoukankan      html  css  js  c++  java
  • POJ 2104 K-th Number(区间第k大数)(平方切割,归并树,划分树)

    题目链接:

    http://poj.org/problem?

    id=2104

    解题思路:

    由于查询的个数m非常大。朴素的求法无法在规定时间内求解。

    因此应该选用合理的方式维护数据来做到高效地查询。

    假设x是第k个数,那么一定有

    (1)在区间中不超过x的数不少于k个

    (2)在区间中小于x的数有不到k个

    因此。假设能够高速求出区间里不超过x的数的个数。就能够通过对x进行二分搜索来求出第k个数是多少。

    接下来,我们来看一下怎样计算在某个区间里不超过x个数的个数。

    假设不进行预处理,那么就仅仅能遍历一遍全部元素。

    还有一方面,假设区间是有序的。那么就能够通过二分搜索法高效地求出不超过x的数的个数了。可是,假设对于每一个查询都分别做一次排序,就全然无法减少复杂度。所以,能够考虑使用平方切割和线段树进行求解。

    1.平方切割

    首先我们来看看怎样使用平方切割来解决问题。

    把数列每b个一组分到各个桶里。每个桶内保存有排序后的数列。这样。假设要求在某个区间中不超过x的数的个数。就能够这样求得。

    (1)对于全然包括在区间内的桶。用二分搜索法计算。

    (2)对于全部的桶不全然包括在区间内的元素,逐个检查。

    假设把b设为sqrt(b),复杂度就变成了

    O((n/b)logb + b) = O(sqrt(n)logn)

    当中。对每一个元素的处理仅仅要O(1)时间,而对于每一个桶的处理则须要O(logb),所以比起让桶的数量和桶内元素的个数尽可能接近。我们更应该把桶的数量设置成比桶内元素个数略少一些,这样能够使得程序更加高效。假设把b设为sqrt(nlogn)。复杂度就变成

    O((n/b)logb + b) = O(sqrt(nlogn))

    接下来仅仅须要对x进行二分搜索就能够了。

    由于答案一定时数列a里的某个元素,所以二分搜索须要运行O(logn)次。因此。假设b = sqrt(nlogn),包含预处理在内整个算法的复杂度就是O(nlogn + msqrt(n)log1.5次方(n))

    AC代码:

    #include <iostream>
    #include <cstdio>
    #include <vector>
    #include <algorithm>
    using namespace std;
    
    const int B = 1000;//桶的大小
    const int N = 100005;
    const int M = 5005;
    //输入
    int n,m;
    int a[N];
    int L[M],R[M],K[M];
    
    int nums[N];//对A排序之后的结果
    vector<int> bucket[N/B];//每个桶排序之后的结果
    
    void solve(){
        for(int i = 0; i < n; i++){
            bucket[i/B].push_back(a[i]);
            nums[i] = a[i];
        }
        sort(nums,nums+n);
        //尽管每B个一组剩下的部分所在的桶没有排序。可是不会产生问题
        for(int i = 0; i < n/B; i++)
            sort(bucket[i].begin(),bucket[i].end());
        for(int i = 0; i < m; i++){
            //求[l,r]区间中第k个数
            int l = L[i]-1,r = R[i],k = K[i];
    
            int lb = -1,ub = n-1;
            while(ub-lb > 1){
                int mid = (lb+ub)/2;
                int x = nums[mid];
                int tl = l,tr = r,c = 0;
    
                //区间两端多出的部分
                while(tl < tr && tl % B != 0)
                    if(a[tl++] <= x)
                        c++;
                while(tl < tr && tr % B != 0)
                    if(a[--tr] <= x)
                        c++;
    
                //对每个桶进行计算
                while(tl < tr){
                    int b = tl/B;
                    c += upper_bound(bucket[b].begin(),bucket[b].end(),x)-bucket[b].begin();
                    tl += B;
                }
    
                if(c >= k)
                    ub = mid;
                else
                    lb = mid;
            }
            printf("%d
    ",nums[ub]);
        }
    }
    
    int main(){
        while(~scanf("%d%d",&n,&m)){
            for(int i = 0; i < n; i++)
                scanf("%d",&a[i]);
            for(int i = 0; i < m; i++)
                scanf("%d%d%d",&L[i],&R[i],&K[i]);
            solve();
        }
        return 0;
    }
    

    2.归并树

    以下我们考虑一下怎样使用线段树解决问题。我们把数列用线段树维护起来。

    线段树的每一个节点都保存了相应区间排好序后的结果。曾经我们接触过的线段树节点上保存的都是数值,而这次则有所不同。每一个节点保存了一个数列。

    建立线段树的过程和归并排序类似,而每一个节点的数列就是其两个儿子节点的数列合并后的结点。建树的复杂度是O(nlogn)。顺带一提,这颗线段树正是归并排序的完总体现。

    (归并树)。

    要计算在某个区间中不超过x的数的个数,仅仅须要递归地进行例如以下操作就能够了。

    (1)假设所给的区间和当前节点的区间全然没有交集。那么返回0个。

    (2)假设所给的区间全然包括了当前节点相应的区间。那么使用二分搜索法对该节点上保存的数组进行查找。

    (3)否则对两个儿子递归地进行计算之后求和就可以。

    因为对于同一深度的节点最多仅仅訪问常数个,因此能够在O(log二次方n)时间里求出不超过x的数的个数。所以整个算法的复杂度是O(nlogn + mlog三次方(n))。

    归并树

    以1 5 2 6 3 7为例:

    把归并排序递归过程记录下来即是一棵归并树:

           [1 2 3 5 6 7]

        [1 2 5]      [3 6 7]

       [1 5] [2]    [6 3] [7] 

      [1][5]        [6][3]

    用相应的下标区间建线段树:(这里下标区间相应的是原数列)

                [1 6]

         [1 3]      [4 6]

      [1 2] [3]   [4 5][6]

      [1][2]      [4][5]

    每次查找[l r]区间的第k大数时,在[1 2 3 4 5 6 7]这个有序的序列中二分所要找的数x,然后相应到线段树中去找[l r]中比x小的数有几个,即x的rank。由

    于线段树中随意区间相应到归并树中是有序的,所以在线段树中的某个区间查找比x小的数的个数也能够用二分在相应的归并树中找。这样一次查询的

    时间复杂度是log(n)^2。

    要注意的是,多个x有同样的rank时,应该取最大的一个。

    AC代码:

    #include <iostream>
    #include <cstdio>
    using namespace std;
    
    const int N = 100005;
    struct node{
        int l,r;
    }tree[N<<2];
    int n,q;
    int a[N],mer[20][N];
    
    void build(int m,int l,int r,int deep){
        tree[m].l = l;
        tree[m].r = r;
        if(l == r){
            mer[deep][l] = num[l];
            return;
        }
        int mid = (l+r)>>1;
        build(m<<1,l,mid,deep+1);
        build(m<<1|1,mid+1,r,deep+1);
        //归并排序,在建树的时候保存
        int i = l,j = (l+r)/2+1,p = 1;
        while(i <= (l+r)/2 && j <= r){
            if(mer[deep+1][i] > mer[deep+1][j])
                mer[deep][p++] = mer[deep+1][j++];
            else
                mer[deep][p++] = mer[deep+1][i++];
        }
        while(i <= (l+r)/2)
            mer[deep][p++] = mer[deep+1][i++];
        while(j <= r)
            mer[deep][p++] = mer[deep+1][j++];
    }
    
    int query(int m,int l,int r,int deep,int key){
        if(tree[step].r < l || tree[m].l > r)
            return 0;
        if(tree[m].l >= l && tree[m].r <= r)
            //找到key在排序后的数组中的位置
            return lower_bound(&mer[deep][tree[m].l],&mer[deep][tree[m].r+1,key) - &mer[deep][tree[m].l];  
        return query(m<<1,l,r,deep+1,key)+query(m<<1|1,l,r,key);
    }
    
    int solve(int l,int r,int k){
        int low = 1,high = n,mid;
        while(low < high){
            mid = (low+high+1)>>1;
            int cnt = query(1,l,r,1,mer[1][mid]);
            if(cnt <= k)
                low = mid;
            else
                high = mid-1;
        }
        return mer[1][low];
    }
    
    int main(){
        while(~scanf("%d%d",&n,&q)){
            for(int i = 1; i <= n; i++)
                scanf("%d",&a[i]);
            build(1,1,n,1);
            while(q--){
                int l,r,k;
                scanf("%d%d%d",&l,&r,&k);
                printf("%d
    ",solve(l,r,k-1));
            }
        }
        return 0;
    }

    3.划分树

    事实上。归并树是在建树的过程中保存归并排序,划分树是在建树的过程中保存高速排序。

    划分树

    相同以1 5 2 6 3 7为例:

    依据中位数mid。将区间划分成左子树中的数小于等于mid。右子树中的数大于等于mid。得到这样一棵划分树:

            [1 5 2 6 3 7]

         [1 2 3]      [5 6 7]

       [1 2]  [3]    [5 6] [7]

      [1] [2]        [5] [6] 

    注意要保持下标的先后顺序不变

    对每个区间。用sum[i]记录区间的左端点left到i有几个进入了左子树,即有几个数小于等于mid

    用相应的下标区间建线段树:(这里下标区间相应的是排序后的数列)

                [1 6]

         [1 3]      [4 6]

      [1 2] [3]   [4 5][6]

      [1][2]      [4][5]

    每次查找[l r]区间的第k大数时。先查看当前区间[left right]下的sum[r] - sum[l - 1]是否小于等于k,假设是,则递归到左子树,并继续在[left + sum[l - 1], 

    left + sum[r] - 1]中找第k大数。否则,进入右子树,继续在[mid + l - left + 1 - sum[l - 1], mid + r - left + 1 - sum[r]]找第k - sum[r] + sum[l - 1]大数,这样

    一次查询仅仅要logn的复杂度


    AC代码:

    #include <iostream>
    #include <cstdio>
    #include <algorithm>
    using namespace std;
    
    const int N = 100005;
    struct node{
        int l,r,mid;
    }tree[N<<2];
    int sa[N],num[20][N],cnt[20][N];//sa中是排序后的,num记录每一层的排序结果。cnt[deep][i]表示第deep层。前i个数中有多少个进入左子树
    int n,q;
    void debug(int d){
        for(int i = 1; i <= n; i++)
            printf("%d ",num[d][i]);
        printf("
    ");
    }
    
    void build(int m,int l,int r,int deep){
        tree[m].l = l;
        tree[m].r = r;
        if(l == r)
            return ;
        int mid = (l+r)>>1;
        int mid_val = sa[mid],lsum = mid-l+1;
        for(int i = l; i <= r; i++)
            if(num[deep][i] < mid_val)
                lsum--;//lsum表示左子树中还须要多少个中值
        int L = l,R = mid+1;
        for(int i = l; i <= r; i++){
            if(i == l)
                cnt[deep][i] = 0;
            else
                cnt[deep][i] = cnt[deep][i-1];
            if(num[deep][i] < mid_val || (num[deep][i] == mid_val && lsum > 0)){
                //左子树
                num[deep+1][L++] = num[deep][i];
                cnt[deep][i]++;
                if(num[deep][i] == mid_val)
                    lsum--;
            }
            else
                num[deep+1][R++] = num[deep][i];
        }
        //debug(deep);
        build(m<<1,l,mid,deep+1);
        build(m<<1|1,mid+1,r,deep+1);
    }
    
    int query(int m,int l,int r,int deep,int k){
        if(l == r)
            return num[deep][l];
        int s1,s2;//s1为[tree[step].left,l-1]中分到左子树的个数
        if(tree[m].l == l)
            s1 = 0;
        else
            s1 = cnt[deep][l-1];
        s2 = cnt[deep][r]-s1;//s2为[l,r]中分到左子树的个数
        if(k <= s2)//左子树的数量大于k,递归左子树
            return query(m<<1,tree[m].l+s1,tree[m].l+s1+s2-1,deep+1,k);
        int b1 = l-1-tree[m].l+1-s1;//b1为[tree[m].l,l-1]中分到右子树的个数
        int b2 = r-l+1-s2;   //b2为[l,r]中分到右子树的个数
        int mid = (tree[m].l+tree[m].r)>>1;
        return query(m<<1|1,mid+1+b1,mid+1+b1+b2-1,deep+1,k-s2);
    }
    
    int main(){
        while(~scanf("%d%d",&n,&q)){
            for(int i = 1; i <= n; i++){
                scanf("%d",&num[1][i]);
                sa[i] = num[1][i];
            }
            sort(sa+1,sa+n+1);
            build(1,1,n,1);
            while(q--){
                int l,r,k;
                scanf("%d%d%d",&l,&r,&k);
                printf("%d
    ",query(1,l,r,1,k));
            }
        }
        return 0;
    }
    


  • 相关阅读:
    centos8 安装postresql12
    vs code 开启远程调试步骤
    node 版本管理器 nvs
    Vue I18n Vue.js 的国际化插件+elementUI的使用
    c#结构
    下拉菜单
    使用Convert 类和Parse方法将字符串转换为数值类型
    c# try..... catch
    c#迭代算法
    网页兼容各种浏览器
  • 原文地址:https://www.cnblogs.com/zhchoutai/p/6900679.html
Copyright © 2011-2022 走看看