主席树(入门篇)

主席树

  • write by BigYellowDog
  • 前置知识:线段树、离散化、前缀和、最好还有Splay

主席树是什么?

  • 首先跟你说说这名字的由来。据说,是一位叫fotile主席的大大在写一道题时因为不会划分树就临时yy出一个算法,于是,这算法就这么诞生了。(这就是大佬吗Orz…)
  • 主席树全称叫可持续化线段树,好复杂。其实就是可以重复利用信息的线段树,从而减小空间和时间的开销。举个例子,为了做到重复利用信息,它时这个样子的:

  • 孤独·粲泽博客找的的图片,ta讲的也特别好辣,推荐大家去看看
  • 看到没,上面那就是一棵标准的主席树,是不是跟线段树不太一样,它很多点是共用的
  • so?知道了主席树是啥样子,它到底能干啥??
  • 比如我有这么一个问题:给定N个整数构成的序列,查询指定区间内的第K小值。
  • 暴力:对于每个指定区间,sort后输出(TLE)
  • 那么主席树这种数据结构就可以解决这类问题辣,怎么解决,继续往下看叭QwQ

如何实现主席树?

1. 建树

  1. 将N个数的区间先离散化
  2. 依次按顺序将N个数离散后的数字依次加入主席树
  • (主席树每个结点的val表示它所负责的区间中出现加入的数的次数)
  • 上面那句话很rao口,下面我们来模拟一组数据
  • 现有数列:1 5 2 6 3 7 4,离散化后为1 5 2 6 3 7 4,依次插入
  • 图片摘自bestFy的博客,这位大大的图更助我理解了主席树,感谢!
  • 首先空树建好qwq

  • 插入1, 包括它的区间的sum都++

  • ……….
  • 一直到插完最后一个数4,图就变成了这样:

  • 于是按照这样的规则,一棵主席树就建好啦!

2. 查询

  • 还是上面那个样例,还是上面上个图,假设我要查询2 - 5区间的第3大,怎么办?
  • 太简单辣!,直接拿插完第5个数时的主席树 - 插完第1个数时的主席树,就OK啦!
  • 别慌,细细品味一下,主席树每个结点的含义为它所负责的区间中出现已经加入的数出现的次数,那假设我查询l - r区间,我把第r棵主席树的每个点的val减掉第l - 1棵主席树的每个点的val后得到一个新的树t,那么这棵树t的每个点就表示它所负责的区间中在l-r区间的数出现的次数。
  • 然后你就会发现这不就是前缀和吗?没错,主席树利用了前缀和思想。每插入一个数都是在前一棵树的基础上插入的!
  • 那么我们把l-r区间的数出现的次数在主席树表示出来后,又怎么求第k大呢?
  • 我们可以给主席树的每个结点加个域sum,表示以它为根节点时它子树的个数。
  • 然后从根节点开始:如果数量>=k,就往左子树走,否则就往右子树走,最终会走到叶子结点就找到了
  • 这个点自己细细体会下吧…如果你懂Splay就秒懂了

3. 建树Again

  • 为什么会有建树Again啊!前面不会说过了吗?
  • 那我跟可以跟你说,前面的建树只是思想,实际建树不能那样建,因为你想想,我们对于每插入一个数就建一棵树,那么n个点就要建n棵树,瞬间爆炸
  • 忘了主席树的定义了吗:可以重复利用信息的线段树
  • 我们还没做到重复利用信息啊!怎么做到,继续看叭QwQ
  • 往上翻到那几张图,观察每次插入一个数时变化的地方
  • 你会发现,每次只有根节点到一个叶子结点的一条链上的点val值发生了变化,其它点都不变。所以我们就利用这点,改变的地方就更新,不变的地方就用上一个主席树的信息就好!
  • 下面针对上面那个样例,直接给出真正建树的逐过程图片!(自己画的QwQ)
  • 留坑待填

代码

  • 如果你认为自己懂了上面我所讲的,那么主席树的算法思路你听懂了(这也是最难的地方)
  • 那么下面我就提供一份我自己的代码,题目是模版题LuoguP3834
  • 还有至于代码,我喜欢用结构体,开数组也行的,差不多。还有代码中有一些细节的地方我在正文中并没有提到,不懂的同学先自己想想,实在想不通了就去问问身边的学长或去Google下(我也是这样走过来的)
#include <cstdio>
#include <iostream>
#include <algorithm>
#include <set>
#include <map>
#define maxn 100005 * 2
using namespace std;

set<int> st;
map<int, int> mp1, mp2;
struct Tree {int sum, l, r;} tree[maxn << 5];
int n, m, g, index;
int a[maxn], b[maxn], rt[maxn];

int read()
{
    int x = 0, f = 1; char c = getchar();
    while(c < '0' || c > '9') {if(c == '-') f = -1; c = getchar();}
    while(c >= '0' && c <= '9') {x = x * 10 + c - '0'; c = getchar();}
    return x *= f;
}

int build(int l, int r)
{
    int t = ++index, mid = (l+r) >> 1;
    if (l < r)
    {
        tree[t].l = build(l, mid);
        tree[t].r = build(mid+1, r);
    }
    return t;
}

int update(int last, int l, int r, int x)
{
    int t = ++index, mid = (l+r) >> 1;
    tree[t].l = tree[last].l; tree[t].r = tree[last].r; tree[t].sum = tree[last].sum + 1;
    if (l < r)
    {
        if (x <= mid) tree[t].l = update(tree[last].l, l, mid, x);
        else tree[t].r = update(tree[last].r, mid+1, r, x);
    }
    return t;
}

int ask(int u, int v, int l, int r, int k)
{
    if (l >= r) return l;
    int x = tree[tree[v].l].sum - tree[tree[u].l].sum, mid = (l + r) >> 1;
    if (x >= k) return ask(tree[u].l, tree[v].l, l, mid, k);
    else return ask(tree[u].r, tree[v].r, mid+1, r, k-x);
}

int main()
{
    cin >> n >> m;
    for (int i = 1; i <= n; i ++) a[i] = read(), st.insert(a[i]);

    while(!st.empty()) {mp1[*st.begin()] = ++g, mp2[g] = *st.begin(); st.erase(st.begin());}
    for (int i = 1; i <= n; i ++) rt[i] = update(rt[i-1], 1, g, mp1[a[i]]);

    rt[0] = build(1, g);
    for(int i = 1; i <= m; i++)
    {
        int l = read(), r = read(), k = read();
        printf("%d\n", mp2[ask(rt[l - 1], rt[r], 1, g, k)]);
    }
    return 0;
}

原文地址:https://www.cnblogs.com/BigYellowDog/p/10326652.html

时间: 2024-10-05 05:22:52

主席树(入门篇)的相关文章

Poj 2104(主席树入门

题目:静态查询区间第k大. 主席树入门题目,之前看的很多资料一上来就是动态区间第k大,看得很费劲,后来找了个写得清晰的,感觉静态的还不算难,代码也不长. /* * @author: Cwind */ //#pragma comment(linker, "/STACK:102400000,102400000") #include <iostream> #include <map> #include <algorithm> #include <cs

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

主席树入门详解+题目推荐

主席树学名可持久化线段树,就是这个可持久化,衍生了多少数据结构 为什么会有主席树这个数据结构呢?它被发明是用来解决什么问题的呢? 给定n个数,m个操作,操作类型有在某个历史版本下单点修改,输出某个历史版本下某个位置的值的值,n和m小于等于1e6 乍一看是不是一点头绪也没有.我们先来想想暴力怎么做,暴力存储第i个状态下每个数的值,显然这样做不是TLE就是MLE,我们不妨管这种状态叫做TM双LE. 如果没有这个历史状态显然处理很简单,一个线段树就解决了.那么加上历史状态呢?如果我们优化一下暴力,我们

poj2104求区间第k小,静态主席树入门模板

看了很久的主席树,最后看https://blog.csdn.net/williamsun0122/article/details/77871278这篇终于看懂了 #include <stdio.h> #include<algorithm> using namespace std; typedef long long ll; const int maxn = 1e5+5; int T[maxn],L[maxn*20],R[maxn*20],sum[maxn*20]; //sz[]为原

hdu 5919 主席树入门题

主席树是从右往左初始化,每次把这个数出现过的位置消去,然后在当前位置加一. 然后我的做法是查两遍,第一遍能找出不同的个数,除一半:再用这个值查,一直到底,最后返回位置,比较套路的一题. #include <stdio.h> #include <string.h> #include <iostream> #include <algorithm> #include <vector> #include <queue> #include &l

SPOJ3267--D-query (主席树入门练习)

题意:查找区间内不同数字的个数. 两种做法,一种是 树状数组离线,另一种就是主席树. 树状数组离线操作的链接 http://www.cnblogs.com/oneshot/p/4110415.html 两种方法思路差不多,都是扫一遍,如果这个数曾经出现过那么就 在上次位置-1,如果没有出现过就在 当前位置+1,同时更新该数字的最新的位置. 这样的话,在主席树里面 以x为根的线段树存的就是1-x之间不同的数字的个数.我们只需要查找以r为根的线段树同时大于l的区间内的个数就行了. 给主席跪了,orz

[知识点]主席树入门 区间k值

入坑主席树主要是因为昨天考试的后两道题不可改2333 而且觉得这个还挺有用,于是果断入坑 不过蒟蒻的我只是在上午看了看大概思路,下午开的运动会没时间码,于是晚上码了出来.但是目前只会无修改的区间K值问题,加上又比较抽象,思路屡了半天才刚刚醒悟,于是写下来记录一下. 不扯那么多辣鸡套路,直接说思路打法(以k小值为例): 我们先要以权值建一颗线段树,然后每个节点记录的是这个节点管辖的范围内数列中数的个数. 我们需要建好多好多线段树.简单来讲呢,就是[1,1],[1,2]···[1,n]这么多颗,每棵

poj2104 K-th Number 主席树入门;

题目链接:K-th Number 题解:我们先把数组离散离散化一下,然后先不考虑L,R的区间的关系,我们有一个棵线段树sum[]保存的是第几大到第几大出现的个数,这样我们想要询问这颗线段数的第k大是多少可以在log(n)次下就找到,但是区间的不同,一颗线段树是解决不了的,那我们如何得到L,R区间的sum数组呢?.我们可以建N棵线段树,第一棵树是空树,然后第一个数过来我们再建一课线段树在原来树的基础上,加上这个数对sum数组的贡献,这样从第一个到第N个建N棵线段树建好,我们可以发现sum[]有前缀

主席树入门

主席树又叫可持久化权值线段树,一开始使用来解决第k大的问题,因其发明者黄嘉泰名字的首字母和某人的一样,所以被叫做主席树. 在了解主席树之前,我们先认识一下什么叫做权值线段树. 给你n个数,问你这n个数中第k小的数是哪个.像这种题我们一般都是直接排序然后暴力找,但是我们今天用线段树来试试. 例如a[12]={1,5,7,3,2,6,8,1,3,5,5,2},你要求出a数组中第7小的数的值.我们先建棵线段树用来存每个值出现的次数,按下标从小到大依次将数组中的元素放入插入线段树中. 插入前三个数后,线

主席树入门野生动物园

有一个很大的野生动物园.这个动物园坐落在一个狭长的山谷内,这个区域从南到北被划分成N个区域,每个区域都饲养着一头狮子.这些狮子从北到南编号为1,2,3,…,N.每头狮子都有一个觅食能力值Ai,Ai越小觅食能力越强.饲养员cmdButtons决定对狮子进行M次投喂,每次投喂都选择一个区间[I,J],从中选取觅食能力值第K强的狮子进行投喂.值得注意的是,cmdButtons不愿意对某些区域进行过多的投喂,他认为这样有悖公平.因此cmdButtons的投喂区间是互不包含的.你的任务就是算出每次投喂后,