POJ 2104 K-th number 主席树 函数式线段树

版权声明:也还是请注明一下原地址,虽然写的不是很好。。。 https://blog.csdn.net/Good_night_Sion_/article/details/72919960



Language:
K-th Number
Time Limit: 20000MS   Memory Limit: 65536K
Total Submissions: 57006   Accepted: 19670
Case Time Limit: 2000MS

Description

You are working for Macrohard company in data structures department. After failing your previous task about key insertion you were asked to write a new data structure that would be able to return quickly k-th order statistics in the array segment. 
That is, given an array a[1...n] of different integer numbers, your program must answer a series of questions Q(i, j, k) in the form: "What would be the k-th number in a[i...j] segment, if this segment was sorted?" 
For example, consider the array a = (1, 5, 2, 6, 3, 7, 4). Let the question be Q(2, 5, 3). The segment a[2...5] is (5, 2, 6, 3). If we sort this segment, we get (2, 3, 5, 6), the third number is 5, and therefore the answer to the question is 5.

Input

The first line of the input file contains n --- the size of the array, and m --- the number of questions to answer (1 <= n <= 100 000, 1 <= m <= 5 000). 
The second line contains n different integer numbers not exceeding 10 9 by their absolute values --- the array for which the answers should be given. 
The following m lines contain question descriptions, each description consists of three numbers: i, j, and k (1 <= i <= j <= n, 1 <= k <= j - i + 1) and represents the question Q(i, j, k).

Output

For each question output the answer to it --- the k-th number in sorted a[i...j] segment.

Sample Input

7 3
1 5 2 6 3 7 4
2 5 3
4 4 1
1 7 3

Sample Output

5
6
3

Hint

This problem has huge input,so please use c-style input(scanf,printf),or you may got time limit exceed.


题意很简单,就是求一个数组里区间L~R的第K小的数。

因为最近看到一道主席树套树状数组的结合题,正好最近也还做了一些线段树的题,就想着把主席树给看一看

我大概解释一下主席树的发明的想法........以下是自己的理解

首先不考虑区间的问题,就考虑整个数组上的第k小数怎么解决,虽然说有线性时间选择的算法,但是现在要考虑用线段树来解决这个问题

用线段树是这么做的,把数组上出现的数字离散化,无妨说离散化到一个新数组dis,那么线段树就是基于这个数组,线段树维护的值就是dis的某个区间内的数字在原数组出现的总次数。

举个例子,原数组是2 3 4 5 6 7 8 9,离散化后的dis是1 2 3 4 5 6 7 8,那么这一棵线段树的根节点的值就是8(代表dis上区间1~8里的数在原数组中一共出现了8次),其左子的值是4(代表dis上区间1~4内的数在原数组出现了4次),右儿子也是4(代表dis上区间5~8内的数在原数组出现了4次)........

在具体查询第k小的数的时候,假若k的值要小于等于左儿子的值,那么就代表最后的答案在左儿子维护的区间里,反之,就在右儿子的区间里面,最后走到叶节点时就是第k小。

现在考虑带有区间的情况,这里又有另外一个性质,我们可以发现,对于我们刚刚说的线段树,dis上的区间L1~R1在原数组L2~R2上的出现次数等于dis上的区间L1~R1在原数组1~R2的出现次数减去在原数组1~L2-1上的出现次数,实际上就是前缀和的性质,只要我们构造出了前缀和,我们可以得到dis上的区间L1~R1在原数组在任意区间上的出现次数。

基于这个想法,我们可以对于每一个区间1~L建立一棵线段树,然后在查询某个区间的时候,用后面的线段树上的值减去前面线段树上的值就是dis数组在该区间上出现的真实次数。

但是在实现的时候还要考虑其他的问题,那就是这样直接建造的空间复杂度是n^2的,过于庞大。实际上我们可以发现,对于区间1~L和区间1~L+1的线段树,也就是前后相邻的线段树,它们有很大一部分是一样的,具体的说,从根节点到某个叶子节点这一条链是不一样的,其余的都是一样的,因为前后只相差1,插入的时候只修改了一条链嘛。

那么我们只构造这一条链,其余的指向原线段树就好。

这就是主席树。

老实说还有人叫它函数式线段树,我很想让人讲讲,为什么叫函数式线段树.......

以下是代码,把查询和插入都写成迭代的写法了,查询的递归写法注释掉了。

#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>
#define mid ((le+ri)>>1)

using namespace std;
const int maxm=1E5+10,maxn=maxm*20;

vector<int> dis;
int n,m,x,y,k,sz,node[maxm],arr[maxm],st[maxn],ls[maxn],rs[maxn],_ins(int y,int p,int d),query(int le=1,int ri=dis.size());
inline int getid(int x){return lower_bound(dis.begin(),dis.end(),x)-dis.begin()+1;}

int main(){
    ios_base::sync_with_stdio(0);
    while(cin>>n>>m){
        for(int i=1;i<=n;++i)
            cin>>arr[i],dis.push_back(arr[i]);
        sort(dis.begin(),dis.end());
        dis.erase(unique(dis.begin(),dis.end()),dis.end());

        for(int i=1;i<=n;++i)
            node[i]=_ins(node[i-1],getid(arr[i]),1);

        while(m--){
            cin>>x>>y>>k;
            x=node[--x],y=node[y];
            cout<<dis[query()-1]<<endl;
        }
        dis.clear();
        sz=0;
    }
    return 0;
}

int _ins(int y,int p,int d){
    int x=++sz,rt=x,le=1,ri=dis.size();
    st[x]=st[y]+d;

    while(le<ri){
        if(p<=mid){
            ls[x]=++sz,rs[x]=rs[y];
            x=ls[x],y=ls[y],ri=mid;
        }else{
            rs[x]=++sz,ls[x]=ls[y];
            x=rs[x],y=rs[y],le=mid+1;
        }
        st[x]=st[y]+d;
    }
    return rt;
}

int query(int le,int ri){
    if(le==ri)
        return le;

    int num;//=st[ls[y]]-st[ls[x]];
    while(le<ri){
        num=st[ls[y]]-st[ls[x]];
        if(num>=k)
            x=ls[x],y=ls[y],ri=mid;
        else
            x=rs[x],y=rs[y],k-=num,le=mid+1;
    }
    return le;
//    if(num>=k){
//        x=ls[x],y=ls[y];
//        return query(le,mid);
//    }else{
//        x=rs[x],y=rs[y],k-=num;
//        return query(mid+1,ri);
//    }
}

PS:

我们可以发现,在这个过程中,仅仅有查询,是没有修改的,那假若有修改怎么办呢?

举个例子,给定数组,查询第K次操作前的区间和,单点更新

那么我们就可以按照时间(操作次数)来建立线段树,我们可以发现,按照时间来建立的前后相邻的线段树也只有一条链的不同而已,和上面是一样的。

只要理解的话,就可以很好的迁移了。


猜你喜欢

转载自blog.csdn.net/Good_night_Sion_/article/details/72919960