基础主席树

我觉得数据结构比其他东西有趣多了,所以我现在沉迷数据结构...

正题:

主席树

又名可持久化线段树,(其实应该反过来,最后说说这个问题[doge])

建议先掌握线段树

所谓可持久化,顾名思义,就是"持久",也就是运行时间长,

非也,是支持关于历史版本的操作,

举个栗子:

现在给定数列\(a\),以及若干次单元素修改,

每次修改会产生一个版本,可以理解为每次修改会产生一个新的数列,

每次修改还基于另一个版本,即一次修改可能在另一次修改之上

(其实也可能在原序列之上,其实就是对版本"0"的修改),

然后给出若干区间\([l,r]\)以及一个(保证合法的)非负整数\(k\),要求你输出在第k次修改后的版本的某种序列信息,可以是单点元素值,可以是区间和,可以试最大最小值等等

区间长度和操作数如其模板题,这里模板题名字是"可持久化数组,但它实际上真的是可持久化线段树的真身"

\(n,m \leqslant 1000000\)

遇到这种又毒瘤又难的问题,我们需要微笑着面对它先思考暴力进而分析正解,

在相当一部分的题目中,我们是可以根据暴力的思路推出正解

基本实现思路及原理

那么我们先基于最基本的单点查询进行思考

看到这道题,先想想最暴力的方法:

开个\(1000000*1000000\)的数组暴力存储

显然只能拿非常可爱的暴力分...

其次我们可以考虑稍微优化一下:

开动态数组(vector) \(a[n]\),每次操作时就在对应位置添加值,然后就可以直接查询

有点意思,这应该就是比较优的策略了,

然而这并没有办法操作基于修改的修改,

如果要修改就只能搞并查集之类的骚操作

但这样就是麻烦,就是啰嗦...

那么对于其他需要维护区间信息的操作及查询呢?

对于上面的区间操作,不难看出要搞线段树,

因为要保证时间复杂度更优,我们只能去选择支持各种区间操作的强大数据结构,比如线段树

考虑这样的暴力:

对于每个操作建一棵新树,以维护所有区间信息

别看了空间炸裂,至少\(O[(1e6)^2*4]\)

但是这样做无疑是正确做法,毕竟这样可以正确维护区间信息...

我们能不能向上面的单点查询一样,进行一些优化呢?

我们来考虑一些未曾考虑到的特殊性质:

  • 每次修改只会修改一个元素,

也就是说,每次修改我们有大部分的数列不用更改

即对于区间信息,我们可以在修改的基础上,对于其他未曾修改的元素进行利用,

我们考虑以下思路:

开始建造一棵线段树,作为最初始序列的对应线段树,

然后对于每次修改,我们对于需要修改的元素所在的区间进行修改,造较少的新节点,作为修改元素对应区间的新修改的版本,

因为每次修改都基于某个版本,对于那些没修改的区间,我们只需要把改过的新节点的左右儿子关系啥的处理一下,将其与不含被修改元素的区间建立联系

也就是说,我们把新节点与不需更改的老节点建立联系,作为修改后的区间

这样一来,我们可以把之前不用修改的元素拉过来进行再利用,从而大大降低时间复杂度

理论-函数实现

这里主要维护单点值,学会了单点值维护区间值也就没什么了

把板子题放在这:真正的主席树模板题

有了思路,我们该如何实现呢?

首先要暂时遗忘以往的堆式存储线段树,就是这样的存储:

p.s.这里字不大清楚,他们分别是\(k<<1\),\(k<<1|1\)

因为在主席树中,所有节点的编号是混乱的,因为我们可能需要把之前的节点拉过来做新节点的儿子,所以并不能很好地确立

  • 先看下需要的变量:
int rt[N<<5];
struct node{
    int ls,rs,val;
}nd[N<<5];
int cnt;
int a[N];

\(cnt\)是用来开点的,作用类似于栈的\(top\)啥的,每次建立一个新节点就\(++cnt\)

对于结构体\(nd\)里的元素,\(ls,rs\)是左右儿子,\(val\)是节点权值,就是区间单点元素值

\(a\)数组就是原序列

结合代码看下新建点方式:

  • 先是建树函数\(build\)
inline int build(ci l,ci r){
    int k=++cnt;
    if(l==r){nd[k].val=a[l];return k;}
    int mid=l+r>>1;
    nd[k].ls=build(l,mid);
    nd[k].rs=build(mid+1,r);
    return k;
}

这里与普通建树不一样的地方在于每次\(build\)函数会返回一个值,这个值是新建节点的编号,用于处理父子关系,

就是把当前节点的编号返回上去,让其父亲将其建立为儿子

对于建树,对于只维护单点权值的题目,非叶节点维护\(val\)没有意义

那么我们要怎么处理修改呢?

考虑下面这样一棵树:

用以上\(build\)函数建出来就是这个鬼样子,

假如我们要修改序列元素\(a[3]\),在图中对应节点\(6\),

我们发现包含节点\(6\)对应元素的节点(线段树区间)就是它的所有父节点,

所以我们只用对其每一个父节点建立新节点,然后再将原有元素利用起来,

就像这样:

括号内表示原节点,

这样我们就用3个节点的超小空间完成了原来要重建一棵树的巨麻烦操作

来看代码:

  • 修改函数\(insert\)
inline int insert(ci k,ci l,ci r,ci x,ci v){
    int nk=++cnt;
    nd[nk]=nd[k];
    if(l==r){nd[nk].val=v;return nk;}
    int mid=l+r>>1;
    if(x<=mid) nd[nk].ls=insert(nd[nk].ls,l,mid,x,v);
    else nd[nk].rs=insert(nd[nk].rs,mid+1,r,x,v);
    return nk;
}

跟\(build\)函数差不多,就是多了一个复制节点操作,

这里新建节点的原理就是复制原版本的节点,然后赋上新值,建立新的儿子,

  • 查询函数\(query\)

返回特定版本特定值,

inline int query(ci k,ci l,ci r,ci x){
    if(l==r) return nd[k].val;
    int mid=l+r>>1;
    if(x<=mid) return query(nd[k].ls,l,mid,x);
    else return query(nd[k].rs,mid+1,r,x);
}

这样基本函数部分基本实现完成了,

完整代码

#include<iostream>
#include<cstdio>
#define ci const int &  //can you follow my speed?
using namespace std;
const int N=1000005;
int n,m;
int rt[N<<5];
struct node{
    int ls,rs,val;
}nd[N<<5];
int cnt;
int a[N];
inline int build(ci l,ci r){
    int k=++cnt;
    if(l==r){nd[k].val=a[l];return k;}
    int mid=l+r>>1;
    nd[k].ls=build(l,mid);
    nd[k].rs=build(mid+1,r);
    return k;
}
inline int insert(ci k,ci l,ci r,ci x,ci v){
    int nk=++cnt;
    nd[nk]=nd[k];
    if(l==r){nd[nk].val=v;return nk;}
    int mid=l+r>>1;
    if(x<=mid) nd[nk].ls=insert(nd[nk].ls,l,mid,x,v);
    else nd[nk].rs=insert(nd[nk].rs,mid+1,r,x,v);
    return nk;
}
inline int query(ci k,ci l,ci r,ci x){
    if(l==r) return nd[k].val;
    int mid=l+r>>1;
    if(x<=mid) return query(nd[k].ls,l,mid,x);
    else return query(nd[k].rs,mid+1,r,x);
}
int main(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
        scanf("%d",&a[i]);
    rt[0]=build(1,n);
    for(int i=1;i<=m;i++){
        int ver,opr,loc;
        scanf("%d%d%d",&ver,&opr,&loc);
        if(opr==1){
            int v;
            scanf("%d",&v);
            rt[i]=insert(rt[ver],1,n,loc,v);
        } else {
            rt[i]=rt[ver];
            printf("%d\n",query(rt[i],1,n,loc));
        }
    }return 0;
}

题目特殊性

对于其模板题这种垃圾无比操作较简单(只有单点查询)的题目,以后应该不会遇到这么简单的

然而对于这种只需要单点查询的题目,我们还是可以说它运用了序列操作,

因为一旦当前修改建立在其他修改之上,这个序列就可能有多个值已经被修改,而不是仅仅用vector就能够解决的

这也是为什么并查集可以做这道题,只要把一串修改所产生的集合并就可以了

最后

这是最基本的主席树,

我讲的好烂啊还是等以后回来update吧...

关于这个数据结构的发明者,他叫黄嘉泰,缩写hjt,自己意会

原文地址:https://www.cnblogs.com/648-233/p/12114487.html

时间: 2024-11-14 12:26:24

基础主席树的相关文章

poj_2104: K-th Number 【主席树】

题目链接 学习了一下主席树,感觉具体算法思路不大好讲.. 大概是先建个空线段树,然后类似于递推,每一个都在前一个"历史版本"的基础上建立一个新的"历史版本",每个历史版本只需占用树高个空间(好神奇!) 查询时这道题是通过"历史版本"间作差解决 *另外提一下,在建立"历史版本"的过程中,是"新建",而不是"更新",是先copy过来原有的,再按个人需求改成自己的,所产生的一个新的"

poj2104(主席树讲解)

今天心血来潮,突然想到有主席树这个神奇的玩意儿...一直都只是听说也没敢看.(蒟蒻蛋蛋的忧伤...) 然后到网上翻大神的各种解释...看了半天... 一拍脑袋...哇其实主席树 真的难...[咳咳我只是来搞笑的] 看了很多种解释最后一头雾水啊...就是没法脑补出(嗯没错经常脑补数据结构长啥样)主席树的样子.... 最后终于找到了一个大大大大大大神犇的ppt,看到了主席树的真面目,才真的能弄懂主席树的结构... -------------------------------------------

(中等) Hiho 1232 Couple Trees(15年北京网络赛F题),主席树+树链剖分。

"Couple Trees" are two trees, a husband tree and a wife tree. They are named because they look like a couple leaning on each other. They share a same root, and their branches are intertwined. In China, many lovers go to the couple trees. Under t

bzoj1901--树状数组套主席树

树状数组套主席树模板题... 题目大意: 给定一个含有n个数的序列a[1],a[2],a[3]--a[n],程序必须回答这样的询问:对于给定的i,j,k,在a[i],a[i+1],a[i+2]--a[j]中第k小的数是多少(1≤k≤j-i+1),并且,你可以改变一些a[i]的值,改变后,程序还能针对改变后的a继续回答上面的问题.你需要编一个这样的程序,从输入文件中读入序列a,然后读入一系列的指令,包括询问指令和修改指令.对于每一个询问指令,你必须输出正确的回答. 第一行有两个正整数n(1≤n≤1

bzoj 3545&amp;&amp;3551: [ONTAK2010]Peaks &amp;&amp;加强版 平衡树&amp;&amp;并查集合并树&amp;&amp;主席树

3545: [ONTAK2010]Peaks Time Limit: 10 Sec  Memory Limit: 128 MBSubmit: 635  Solved: 177[Submit][Status] Description 在Bytemountains有N座山峰,每座山峰有他的高度h_i.有些山峰之间有双向道路相连,共M条路径,每条路径有一个困难值,这个值越大表示越难走,现在有Q组询问,每组询问询问从点v开始只经过困难值小于等于x的路径所能到达的山峰中第k高的山峰,如果无解输出-1. I

POJ 2104&amp;HDU 2665 Kth number(主席树入门+离散化)

K-th Number Time Limit: 20000MS   Memory Limit: 65536K Total Submissions: 50247   Accepted: 17101 Case Time Limit: 2000MS Description You are working for Macrohard company in data structures department. After failing your previous task about key inse

spoj cot: Count on a tree 主席树

10628. Count on a tree Problem code: COT You are given a tree with N nodes.The tree nodes are numbered from 1 to N.Each node has an integer weight. We will ask you to perform the following operation: u v k : ask for the kth minimum weight on the path

【BZOJ 3123】 [Sdoi2013]森林 主席树启发式合并

我们直接按父子关系建主席树,然后记录倍增方便以后求LCA,同时用并查集维护根节点,而且还要记录根节点对应的size,用来对其启发式合并,然后每当我们合并的时候我们都要暴力拆小的一部分重复以上部分,总时间复杂度为O(n*log),因为每个的节点只会作为小的部分合并,因此他所在的一小部分至少变大2倍,对于每一个节点他作为被合并方最多log次,因此其复杂度为O(n*log),而这个是绝对跑不满还差很多的,我们视他为无常数就好了,当然这一切都是建立在无拆分操作的基础之上,只要有了拆分启发式合并的复杂度就

【模板】主席树

主席树..高大上的名字..原名叫可持久化线段树..也有人叫函数式线段树(其实叫什么都不重要). 本来的作用就是字面意思..持久化的线段树,支持修改之后查找某次修改之前的版本.(在NOIP之前在算法导论上看到过,当时觉得没什么,现在才知道好厉害的数据结构) 具体来怎么实现呢..其实就是每次修改的时候都新开一个根节点然后把和修改有关的区间一路新建下去,与修改无关的区间就继承上次修改版本的节点,这样会节省空间. 来应用一下,一个基础的应用就是区间K小值查询(poj2104).即给定一个序列,给出L,R