主席树(入门篇)

主席树

  • 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;
}

猜你喜欢

转载自www.cnblogs.com/BigYellowDog/p/10326652.html