【POJ 2104/HDU 2665】K-th Number【整体二分/主席树】

【POJ 2104/HDU 2665】K-th Number【整体二分/主席树】

题意

<script id="MathJax-Element-1" type="math/tex">\;\;\;\;</script>给出一个数组,多次查询区间 [l,r] <script id="MathJax-Element-2" type="math/tex">[l, r]</script>内的第 k <script id="MathJax-Element-3" type="math/tex">k</script>小数。

思路

1. <script id="MathJax-Element-4" type="math/tex">1.</script>主席树

<script id="MathJax-Element-5" type="math/tex">\;\;\;\;</script>一贯地思考的话,主席树是显然可做此题的:用第 i <script id="MathJax-Element-6" type="math/tex">i</script>个版本维护该数组第 i <script id="MathJax-Element-7" type="math/tex">i</script>个前缀的值域线段树,每次查询只需在两个版本的差分线段树上二分即可。

2. <script id="MathJax-Element-8" type="math/tex">2.</script>整体二分

<script id="MathJax-Element-9" type="math/tex">\;\;\;\;</script>不妨换位思考:主席树的过程实际上是先把区间信息分离出来,再进行二分答案。若是把这个先后顺序调转一下,即是整体二分的思想了。
<script id="MathJax-Element-10" type="math/tex">\;\;\;\;</script>何言整体?对于单个询问,为了判断二分的选择肢,我们得把整个区间遍历一遍,这就造成了最坏情况下 O(n) <script id="MathJax-Element-11" type="math/tex">O(n)</script>的时间复杂度。但若是我们能同时判断多个询问的选择肢,以将 O(n) <script id="MathJax-Element-12" type="math/tex">O(n)</script>的时间均摊开来,就成为了一种可行的算法。这即是整体二分的思想。

Codeisalwaysthefirstpriority: <script id="MathJax-Element-13" type="math/tex">\;\;\;\;Code\;is\;always\;the\;first\;priority:</script>

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
#define re_ return
#define inc(l, i, r) for(i=l; i<r; ++i)

const int mxn=1<<17;

int n, m, b[mxn], ans[mxn];
struct que{int l, r, k, i;} a[mxn], q[mxn], ql[mxn], qr[mxn];//单个查询
inline bool operator <(const que& a, const que& b)
{re_ a.k<b.k;}

//---树状数组
inline char add(int i, int x){for(;i<=n; i+=i&-i) b[i]+=x;}
inline int sum(int i)
{
    static int r;
    for(r=0; i; i-=i&-i) r+=b[i];
    re_ r;
}
//---

char div(int s, int t, int l, int r)//在查询队列中的首尾地址和二分值域左右端点
{
    int i, x, y, z, mid=l+r>>1, tl=0, tr=0;

    if(s==t || l==r-1)
    {
        inc(s, i, t) ans[q[i].i]=l;
        re_ 0;
    }
    x=lower_bound(a, a+n, (que){0, 0, l, 0})-a;
    y=lower_bound(a, a+n, (que){0, 0, mid, 0})-a;

    inc(x, i, y) add(a[i].i, 1);//仅遍历在左值域内的元素,插入到对应位置的树状数组中
    inc(s, i, t)//将答案在当前二分值域内的查询分为答案在左右子值域内的查询
        if((z=sum(q[i].r)-sum(q[i].l-1))<q[i].k)//判断同时在左值域和该查询区间内的元素的个数是否小于k
            q[i].k-=z, qr[tr++]=q[i];//注意,当该查询答案在右值域时,要把左值域内的元素对“第k小”的贡献考虑到。表现为“k-=z”。
        else
            ql[tl++]=q[i];
    inc(x, i, y) add(a[i].i, -1);//树状数组是全局的,需要清空
    inc(0, i, tl) q[s+i]=ql[i];
    inc(0, i, tr) q[s+tl+i]=qr[i];
    div(s, s+tl, l, mid), div(s+tl, t, mid, r);//递归处理左右子区间
}

int main()
{
    int i;
    scanf("%d%d", &n, &m);
    inc(0, i, n) scanf("%d", &a[i].k), a[i].i=i+1;
    sort(a, a+n);//以值为关键字排序

    inc(0, i, m)
        scanf("%d%d%d", &q[i].l, &q[i].r, &q[i].k), q[i].i=i;

    div(0, m, -(1<<30), 1<<30);
    inc(0, i, m) printf("%d\n", ans[i]);
    re_ 0;
}

复杂度分析

<script id="MathJax-Element-14" type="math/tex">\;\;\;\;</script>首先,递归层数上界是 log2a <script id="MathJax-Element-15" type="math/tex">\log_2a\;</script>( a <script id="MathJax-Element-16" type="math/tex">a</script>不经离散化时是总值域大小,否则是 n <script id="MathJax-Element-17" type="math/tex">n</script>)。由于每个询问都会在每一层恰被判断一次,所以这部分的时间复杂度是 O(mlog2a) <script id="MathJax-Element-18" type="math/tex">O(m\log_2a)</script>。每个元素在每一层被插入到树状数组中的次数为 0 <script id="MathJax-Element-19" type="math/tex">0</script>或 1 <script id="MathJax-Element-20" type="math/tex">1</script>,所以这部分的时间复杂度是 O(nlog2nlog2a) <script id="MathJax-Element-21" type="math/tex">O(n\log_2n\log_2a)</script>。总的来说,这是个 O(nlog22n) <script id="MathJax-Element-22" type="math/tex">O(n\log^2_2n)</script>的优秀离线算法,往往能避免写诸如主席树或树套树之类的复杂数据结构。

©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页