主席树学习

普及贴,大牛绕道。。。。

主席树简直神,看了很多关于这个算法的资料,还是不能一下子明白,建议学习主席树的时候,算法讲解的话,直接眼睛扫一遍就好了,不要指望别人能讲清楚,还是要自己看代码理解。

老规矩,从这个题 POJ 2104 说起,题意求任意区间的第 K 大。不带修改的静态的主席树。
以数据:

5
5 2 1 4 3
2 5 3

为例

首先我们呢需要把所有的数离散化一下。。

从代码看起:

建树

int build(int L,int R)
{
    int rt = tot++;
    c[rt] = 0;
    if(L != R) {
        int mid = (L + R) >> 1;
        lson[rt] = build(L, mid);   
        rson[rt] = build(mid + 1, R);
    }
    return rt;
}

这段代码呢,就是建一棵树,同时记录下每个节点的编号以及其左右儿子的编号。如下:
建树

2.原数组中从后往前依次建树

//线段树上从 rt 节点到 pos 在节点 路径上 生成新的节点
//同时维护 原线段树 与 新生成节点 同位置上的 c数组值
//即c[newRoot] = c[root] + val;
//同时T[i] 记录新路径 的起始节点
int update(int rt,int pos,int val)
{
    int nrt = tot++, tmp = nrt;

    c[nrt] = c[rt] + val;
    int L = 1, R = tol;
    while(L < R){
        int mid = (L + R) >> 1;
        if(pos <= mid) {
            lson[nrt] = tot++, rson[nrt] = rson[rt];

            nrt = lson[nrt], rt = lson[rt];
            R = mid;
        }
        else {
            rson[nrt] = tot++, lson[nrt] = lson[rt];

            nrt = rson[nrt], rt = rson[rt];
            L = mid + 1;
        }
        c[nrt] = c[rt] + val;
    }
    return tmp;
}
//tol是离散化后数组的长度,t数组存的是原数组排好序的数组。
T[n + 1] = build(1, tol);
for(int i = n;i;i --) {
     p = lower_bound(t + 1, t + 1 + tol, a[i]) - t;
     T[i] = update(T[i+1], p, 1);
}

这段代码的解释上面写的很清楚了,以update最后一个数3为例,就会新建成下面的线段树,注意到只有从根节点到3的路径上的点被赋予的新的值,而且同时更新了 c 数组,用处后面自然会看到啦。如下:
update
update4

3.查找区间第K大
个人觉得这里是最难理解的。

int query(int rt_L,int rt_R,int k)
{
    int l = 1, r = tol;
    //每次查询区间 [L,R] 内的数出现在[1, mid] 内的次数
    //随之进行二分
    while(l < r) {
        int mid = (l + r) >> 1;

        if(c[lson[rt_L]] - c[lson[rt_R]] >= k){
            rt_L = lson[rt_L];
            rt_R = lson[rt_R];
            r = mid;
        }
        else {
            k -= c[lson[rt_L]] - c[lson[rt_R]];
            rt_L = rson[rt_L];
            rt_R = rson[rt_R];
            l = mid + 1;
        }
    }
    return l;
}
//找到对应的区间,查询第K大
//从前面可以知道,T数组存的是原数组每个数对应的线段树的顶点编号。
scanf("%d%d%d",&l,&r,&k);
printf("%d\n",t[query(T[l], T[r+1], k)]);

从这里我们就能弄明白前面的c数组到底有什么用,还要要弄清楚一个问题,什么条件下要取 r= mid, 什么条件下要取 l = mid +1,为什么要这么取?能够把这个问题搞明白,就可以去膜拜主席了,Orz。

我们想想 c 数组到底存的是什么,从前面画的图就可以知道当update时经过线段树上某个节点就会有一次c[nrt] = c[rt] + val,而且只有当update的这个数(例如前面的3)在节点对应的区间的时候才会经过这个点,所以每颗线段树上每个节点 c 数组存的就是当前为止 原数组里的数范围在 [L,R] 内出现的次数。(这里是关键,想明白!

所以很自然的,查询第K大的时候,把区间拆半, [L,mid] , [mid+1,R] ,如果区间内的数出现在 [L,mid] 内的次数大于等于 K 的话,说明第K大的数一定在区间 [L,mid] , 否则找区间 [mid+1,R] ,这里的思路就很明显啦!

*以上讲的是不带修改的静态主席树,如果带修改的话,还要弄个树状数组来优化。总的来讲呢,主席树就是很神啦,自己好好体验吧!

4.全部代码

#include <cstdio>
#include <cstring>
#include <queue>
#include <algorithm>
#include <iostream>
#include <cmath>
#include <map>
#include <vector>
#include <set>
#include <string>
#define PB push_back
#define FT first
#define SD second
#define MP make_pair
#define INF 0x3f3f3f3f
using namespace std;
typedef long long LL;
typedef unsigned long long ULL;
typedef pair<int,int>  P;
const int N = 10 + 1e5,M =  30 * N;
int n, q, tol, tot;
int a[N], t[N];
int c[M], T[M], lson[M], rson[M];
void init()
{
    tot = 0;
    for(int i = 1;i <= n;i ++) t[i] = a[i];
    sort(t+1, t+1+n);
    tol = unique(t+1, t+1+n) - t - 1;
}
int build(int L,int R)
{
    int rt = tot++;
    c[rt] = 0;
    if(L != R) {
        int mid = (L + R) >> 1;
        lson[rt] = build(L, mid);   
        rson[rt] = build(mid + 1, R);
    }
    return rt;
}
//线段树上从 rt 节点到 pos 在节点 路径上 生成新的节点
//同时维护 原线段树 与 新生成节点 同位置上的 c数组值
//即c[newRoot] = c[root] + val;
//同时T[i] 记录新路径 的起始节点
int update(int rt,int pos,int val)
{
    int nrt = tot++, tmp = nrt;

    c[nrt] = c[rt] + val;
    int L = 1, R = tol;
    while(L < R){
        int mid = (L + R) >> 1;
        if(pos <= mid) {
            lson[nrt] = tot++, rson[nrt] = rson[rt];

            nrt = lson[nrt], rt = lson[rt];
            R = mid;
        }
        else {
            rson[nrt] = tot++, lson[nrt] = lson[rt];

            nrt = rson[nrt], rt = rson[rt];
            L = mid + 1;
        }
        c[nrt] = c[rt] + val;
    }
    return tmp;
}
int query(int rt_L,int rt_R,int k)
{
    int l = 1, r = tol;
    //每次查询区间 [L,R] 内的数出现在[1, mid] 内的次数
    //随之进行二分
    while(l < r) {
        int mid = (l + r) >> 1;
        // cout << rt_L << " " << rt_R << " | " << c[lson[rt_L]] << " " << c[lson[rt_R]] << endl;
        if(c[lson[rt_L]] - c[lson[rt_R]] >= k){
            rt_L = lson[rt_L];
            rt_R = lson[rt_R];
            r = mid;
        }
        else {
            k -= c[lson[rt_L]] - c[lson[rt_R]];
            rt_L = rson[rt_L];
            rt_R = rson[rt_R];
            l = mid + 1;
        }
    }
    return l;
}
int main()
{
    freopen("in.txt","r",stdin);
    //freopen("out.txt","w",stdout);
    int p, l, r, k;
    while(scanf("%d%d",&n, &q) == 2){
        for(int i = 1;i <= n;i ++) {
            scanf("%d",&a[i]);
        }
        init();
        T[n + 1] = build(1, tol);
        for(int i = n;i;i --) {
            p = lower_bound(t + 1, t + 1 + tol, a[i]) - t;
            T[i] = update(T[i+1], p, 1);
        }
        while(q --) {
            scanf("%d%d%d",&l,&r,&k);
            printf("%d\n",t[query(T[l], T[r+1], k)]);
        }
    }
    //system("pause");
    return 0;
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值