Kth number 主席树(可持久化线段树)

Kth number

 HDU - 2665 
Give you a sequence and ask you the kth big number of a inteval.

InputThe first line is the number of the test cases. 
For each test case, the first line contain two integer n and m (n, m <= 100000), indicates the number of integers in the sequence and the number of the quaere. 
The second line contains n integers, describe the sequence. 
Each of following m lines contains three integers s, t, k. 
s,ts,t indicates the interval and k indicates the kth big number in interval s,ts,tOutputFor each test case, output m lines. Each line contains the kth big number.Sample Input

1 
10 1 
1 4 2 3 5 6 7 8 9 0 
1 3 2 

Sample Output

2 

题意很简单,就是给你n个数,再给你m组询问,问区间[l,r]中第k小的数字是多少。

我们可以先考虑简单情况,假设l和r固定是1和n,要怎么做。不考虑离线版本,如果强制在线的话我们可以建一棵权值线段树,就是维护数字数量的线段树,比如说,有五个数字1,1,2,2,2,那么Tree[1]=2,Tree[2]=3,这样假设我要查询第k小的数,可以选择二分区域,找到sum[T[1~pos]]>=k,那pos就是第k小的数字了。这种方法复杂度是logn*logn,我们可以再优化一下。可以从线段树的根节点开始找,每次向下查找,如果左节点的数字总数大于等于k,说明第k小的数字一定在左节点,我们就往左节点走,否则往右节点走,同时k要减去左节点数字总数,因为线段树维护的不是前缀和,右节点数字总数不包括左节点数字总数。这样不断递归下去,当l==r的时候返回l就行了。这种查询方式复杂度是logn。注意,当数字太大时要进行离散化再建树,否则内存会爆炸。

接下来考虑进化版,l和r是任意数字的时候,我们可以考虑建n棵线段树,SegTree[i]代表插入第i个数字的时候对应的线段树,假设我能得到这n棵线段树,那我查询[l,r]这区间的第k小数字可以利用前缀和用第r棵线段树减去第l-1棵线段树,此时可以得到[l,r]这部分的线段树,然后我们再查询第k小的时候可以用上面讲过的方法查询。关键在于,如果真的建了n棵线段树,内存会爆炸。其实仔细思考我们会发现,当我们添加了一个点后,会影响到线段树的节点只有logn个,我们只需要新建这logn个节点就好,这样空间复杂度就大大降低了。我们用root[i]代表SegTree[i]的根节点,在更新的时候只更新被影响到的logn个节点就好。具体实现看代码,有注释。

 

#include <iostream>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <algorithm>
#include <vector>
#include <queue>
#include <string>
#include <stack>
#include <map>
#include <set>
#include <bitset>
#define X first
#define Y second
#define clr(u,v); memset(u,v,sizeof(u));
#define in() freopen("data","r",stdin);
#define out() freopen("ans","w",stdout);
#define Clear(Q); while (!Q.empty()) Q.pop();
#define pb push_back
using namespace std;
typedef long long ll;
typedef pair<int, int> pii;
const int maxn = 1e5 + 10;
const int INF = 0x3f3f3f3f;
struct Tree
{
    int l, r, sum; //l,r指向左右子树,sum该节点对应的数字的总数量
} T[maxn<<5]; //这是一颗权值线段树,就是根据数字建树,维护数字的个数

int cnt;//线段树总节点数
int N[maxn];
int root[maxn];//root[i]代表第i棵线段树所在位置
vector <int> V;

void init()
{
    cnt = 0;
    V.clear();
    T[0].sum = 0;
}

int id(int x)//用于离散
{
    return lower_bound(V.begin(), V.end(), x) - V.begin() + 1;
}

void update(int l, int r, int &x, int y, int pos)
{
    T[++cnt] = T[y]; //新建节点,并继承他的上一个版本
    T[cnt].sum++;//新加了一个数字,sum++
    x = cnt;//将这节点和他的父节点连起来
    if (l == r) return ;
    int mid = (l + r) >> 1;
    if (mid >= pos) update(l, mid, T[x].l, T[y].l, pos);//折半查找找到pos的位置进行更新,线段树的基本操作
    else update(mid + 1, r, T[x].r, T[y].r, pos);
}

int query(int l, int r, int x, int y, int k) //查询第k大
{
    if (l == r) return l;
    int mid = (l + r) >> 1;
    int sum = T[T[y].l].sum - T[T[x].l].sum;//利用前缀和求出[x,y]这部分的线段树
    //T[T[y].l].sum - T[T[x].l].sum可以得出[x,y]这线段树左子树的sum
    if (sum >= k) return query(l, mid, T[x].l, T[y].l, k);//如果sum>=k说明要找的值在左子树
    else return query(mid + 1, r, T[x].r, T[y].r, k - sum);//否则到右子树去找
}

int main()
{
    int T;
    scanf("%d", &T);
    while (T--)
    {
        init();
        int n, m, l, r, k;
        scanf("%d%d", &n, &m);
        for (int i = 1; i <= n; i++)
        {
            scanf("%d", &N[i]);
            V.pb(N[i]);
        }
        sort(V.begin(), V.end());
        V.erase(unique(V.begin(), V.end()), V.end());
        for (int i = 1; i <= n; i++)
            update(1, n, root[i], root[i-1], id(N[i]));//根据1~n建立n棵线段树,root[i]代表前i棵线段树的根节点,root[i-1]则是上一个版本的线段树
        while (m--)
        {
            scanf("%d%d%d", &l, &r, &k);
            printf("%d\n", V[query(1, n, root[l-1], root[r], k) - 1]); //查询[l,r]这棵线段树的第k小数
        }
    }
    return 0;
}
posted @ 2017-05-16 01:05  酱油党gsh  阅读(230)  评论(0编辑  收藏  举报