主席树基础

主席树明明是个很棒的东西,可惜网上却很难找到很好的模板/讲解

包括卿学姐的讲解的模板,看得云里雾里,就感觉模板不太对劲,怎么边界用的是离散前的?

直至找到https://blog.finaltheory.me/algorithm/Chairman-Tree.html#7865d

发现边界确实直接用离散后的就行,毕竟是实质是权值线段树。

这是第一篇,讲模板,POJ2104,无修改的主席树模板,求区间第K大,主席树的典例

首先是空间,一般来说开<<5(32)倍或者直接40倍会比较好

因为初始为4倍(线段树空间)n,每次更新最坏要logn,加起来就是n(logn+4)的空间,而2的28次方大概是2e8,一般的n都没有这么大(也没有那么大内存),所以32倍空间已经足够了。

首先是前置技能:权值线段树。

权值线段树,顾名思义,就是以权值建树。以往我们的线段树存的是具体的值,而权值线段树,存的是数量,每个结点的sum存储该代表权值在该区间内的点的数量,安利一篇BLOGhttps://blog.csdn.net/Stupid_Turtle/article/details/80445998,图做得很棒,一看就懂。

然后就来到了我们的重点,主席树。

主席树的核心思想是:利用历史版本的线段树,只更新每次修改的内容。首先上一个视频https://www.bilibili.com/video/av4619406/?p=2,卿学姐的讲解,5分钟而已。

再上图

 

update表示我要更新的结点,更新此结点,按照普通的线段树,自然是更新从根结点到此节点整条链上的点的sum值。但是主席树就不一样了,主席树选择新建一条链。那没更新的点呢?继续用以前的就行啦。所以图中虚线链接的两点其实是同一个点,只要将父节点指向原先已经建好且没更新的点即可。下面上的模板是我该篇最上边引用的文章里的,加了些注释,且会进行说明。

#include <cstring>
#include <algorithm>
#include<cstdio>
#define MAX 100010
#define CLR(arr,val) memset(arr,val,sizeof(arr))
using namespace std;
const int INF = 0x3f3f3f3f;
//记录原数组、排序后的数组、每个元素对应的根节点
int nums[MAX], sorted[MAX], root[MAX];
int cnt;
struct TMD
{
    int sum, L_son, R_son;
} Tree[MAX<<5];
inline int CreateNode( int _sum, int _L_son, int _R_son )
{
    int idx = ++cnt;
    Tree[idx].sum = _sum;
    Tree[idx].L_son = _L_son;
    Tree[idx].R_son = _R_son;
    return idx;
}
void Insert( int & root, int pre_rt, int pos, int L, int R ) //相当于更新,每次更新一条链 权值线段树 
{
    //从根节点往下更新到叶子,新建立出一路更新的节点,这样就是一颗新树了。
    root = CreateNode( Tree[pre_rt].sum + 1, Tree[pre_rt].L_son, Tree[pre_rt].R_son ); //指向旧版本的左右孩子 
    if ( L == R ) return;
    int M = ( L + R ) >> 1;
    if ( pos <= M )
        Insert( Tree[root].L_son, Tree[pre_rt].L_son, pos, L, M );
    else
        Insert( Tree[root].R_son, Tree[pre_rt].R_son, pos, M + 1, R );
}
int Query( int S, int E, int L, int R, int K )
{
    if ( L == R ) return L;
    int M = ( L + R ) >> 1;
    //下面计算的sum就是当前询问的区间中,左儿子中的元素个数。
    int sum = Tree[Tree[E].L_son].sum - Tree[Tree[S].L_son].sum;
    if ( K <= sum )
        return Query( Tree[S].L_son, Tree[E].L_son, L, M, K );
    else
        return Query( Tree[S].R_son, Tree[E].R_son, M + 1, R, K - sum );
}
int main()
{
    int n, m, num, pos, T;
    while ( scanf("%d %d", &n, &m) != EOF )
    {
        cnt = 0; root[0] = 0;
        for ( int i = 1; i <= n; ++i )
        {
            scanf("%d", &nums[i]);
            sorted[i] = nums[i];
        }
        sort( sorted + 1, sorted + 1 + n );
        num = unique( sorted + 1, sorted + n + 1 ) - ( sorted + 1 );
        for ( int i = 1; i <= n; ++i )
        {
            //实际上是对每个元素建立了一颗线段树,保存其根节点
            pos = lower_bound( sorted + 1, sorted + num + 1, nums[i] ) - sorted;
            Insert( root[i], root[i - 1], pos, 1, num );
        }
        int l, r, k;
        while ( m-- )
        {
            scanf("%d %d %d", &l, &r, &k);
            pos = Query( root[l - 1], root[r], 1, num, k );
            printf("%d\n", sorted[pos]);
        }
    }
}

这篇模板的重点,自然是insert过程。insert其实就是update,传入的参数pos是对应权值线段树的值。你可以把他当做离散以后,每个叶子结点的sum值就是下标为pos(权值为pos)的点的数量,而区间1~num则代表下标1~num(权值1~num)。

在insert过程里,我们每次要新建一个结点,因为该结点是需要被修改的,就是我们上面所说的在传统线段树需要被修改的链上的一点。但是因为这个是主席树,所以我们要新建而不是修改历史版本的该结点。同时,我们新建的该结点的子结点都指向历史版本(上一个版本)对应的该结点的子结点(这个应该比较好理解),因为我们不确定子结点需不需要更新,或者说,不确定哪个子结点需要更新,哪个不需要。然后继续向下遍历,如果该子结点要更新,那么就重复上述过程,新建一点,把历史版本子结点的子结点(孙子结点)连到我们新建的这个子结点上。

在查询过程,每个结点作差就代表在此查询区间,我每个结点对应的权值区间内的点有几个。比如我要求查询区间第5大的,那假设我左子结点代表权值在【1,2】内的点数量,右子结点代表权值在区间【3,5】的数量。如果我左子结点的sum>=5,就代表权值在【1,2】内的点至少有5个,那么查询区间的第5大一定在左子结点代表的权值区间内产生;反之,自然就在右子结点代表的权值区间内产生。

感觉写得不太好(wtcl),希望大家能看懂QAQ。

最近再把动态的更了,再更几道比较典型的题QWQ(我相信我不会咕咕咕的)。

(我才不会告诉你们其实这篇BLOG在两个月前就写了一半存了草稿,一直咕到现在)

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值