• Yangk's 静态主席树


    主席树

    然后在这先%%%一个B站宝藏up主,数据结构讲的很清楚,主席树这一节也是听了很多很多遍

    https://space.bilibili.com/120174936?spm_id_from=333.788.b_765f7570696e666f.1

    有些概念来自up主的ppt,主席树的代码习惯也是跟up主学的欸嘿嘿

    什么是主席树

    主席树是一种可持久化的数据结构一可持久化线段树(因为其有函数的性质也叫函数式线段树)

    至于为什么叫主席树呢,在这先膜一下黄嘉泰大佬 (HJT) % % % 剩下自行体会

    这篇介绍的是静态主席树,至于带修改主席树以及树上主席树的坑,我以后再补

    怎么实现

    先抛出一个问题,在线询问时,可能会用到之前修改后的值,但他已经被你后几次的修改覆盖掉了,怎么办,把他们都存下来吗

    空间复杂度爆炸的大

    但是,可以发现,两版本之间有很多的相同之处,如果能利用上这些相同的部分,那么就可以节省很大一部分空间,使得空间复杂度下降

    其实,主席树的本质是好多个线段树,我们利用了前缀的思想,对于每一个前缀建一棵树,降低时间复杂度,然后再利用相同的部分把空间复杂度降下来(相当于每次操作只是多了一条链而不是一颗完整的树),达到高效实现问题的效果

    实际上,模板题只是一道静态主席树,利用前缀的思想还可以将主席树与树状数组套在一起,实现带修改主席树,不过这就是后话了

    如何利用之前版本相同的内容

    我们可以在构建新版本的线段树的时候,把没有变动的部分连回到上一个版本的那个部分上(把他左边的树维护的东西复制一遍给他,然后再修改需要修改的值)

    这样就可以实现0浪费了

    我看这个图片就非常的形象

    图片来自:https://www.cnblogs.com/LonecharmRiver/articles/9087536.html

     

     

    这是模板题 洛谷P3834 POJ-2104 K-th Number的解题思路

    在原来的基础上新建一条链 连回另一个子节点 下面我们就来看一下这个题吧

    模板题

    洛谷P3834 POJ-2104 K-th Number

    题目描述

    如题,给定 n 个整数构成的序列,将对于指定的闭区间查询其区间内的第 k小值。

    输入格式

    第一行包含两个正整数 n,m,分别表示序列的长度和查询的个数。

    第二行包含 n 个整数,表示这个序列各项的数字。

    接下来 m 行每行包含三个整数 l, r, k, 表示查询区间 [l, r]内的第 k小值。

    输出格式

    输出包含 m行,每行一个整数,依次表示每一次查询的结果

    数据范围

    1<=n,m<=2e5   -1e9<=a[i]<=1e9

    朴素做法:每次对[l,r]排序再询问o(mnlogn) 铁定是过不去

    HJT大佬做法:对于原序列的每一个前缀[1--i]建立出一棵线段树维护值域上每个数出现的次数,则其树是可减的

    插播:权值线段树,对一个值域上的值的个数进行维护的线段树,

    对于一段数字序列,每次询问其第K大值

    先建树然后对于每次询问递归查找即可。要找的数如果在左子树上,

    就递归查找左子树上的第k大;如果在右子树上,那么就递归查找右子树上的.第(k-左子树值)大

    只是在这个题里,可持久化与权值线段树联系在一起了,但实际还有很多可持久化的问题是维护的别的东西,所以也不用说是对于实现的方式过于较真,怎么解怎么实现还是要看题目

    了解了权值线段树,我们就可以切这道题了

    首先,我们需要先对数据进行离散化。为什么要离散化呢?因为权值线段树维护的是值域,上值的个数,数据范围是-1e9≤a[i]≤1e9,显然开不出这么大的线段树。因为问题只与数的大小相关,而与数本身是多少无关,所以我们可以对数进行离散化,只保存其大小关系即可

    例: 25957 6405 15770 26287 26465离散化结果: 3 1 2 4 5

    然后我们的主要思想是:用主席树构造一颗可持久化权值线段树,对于每个数字,将其离散化后,新建一个版本的权值线段树,然后插入这个离散化后的数字。例如对于上述序列,我们就要依次建立5个版本的权值线段树,分别插入31245这五个数。这就是对原序列的每一个前缀建树’

    对于查询操作L,R,我们利用主席树的函数性质,用R那个版本的权值线段树减去L-1那个版本的权值线段树,在得到的权值线段树中查找第K小值就可以了。有点类似前缀和

    这样为什么是正确的呢?因为权值线段树储存的是值域,上值的个数,我们用R版本的权值线段树减去L-1版本的权值线段树,得到的就是维护[L,R]上值的个数的权值线段树

    得到结论:主席树=权值线段树+可持久化+前缀和

    关于如何实现

    离散化:vector排序去重,用的时候二分查找

     

    排序去重

    sort(ve.begin(),ve.end());
    
    ve.erase(unique(ve.begin(),ve.end()),ve.end());
    
     
    
    getid
    
    int getid(int x){return lower_bound(ve.begin(),ve.end(),x)-ve.begin()+1;}

    如何储存主席树:

    主席树和线段树不一样,因为是动态开点,所以找左右儿子不能通过<<1 and<<1|1来实现,但我们可以用一个结构体存下他的儿子节点的编号,因为是动态开点,最后要开多少点我们也不能确定,但一般都开到35-50左右

    hjt[i]为树上的节点,cnt为动态开点的计数器,root[i]为根结点编号,一个root[i]就是一个新版本

    struct node
    {
        int l,r,sum;
    
    }hjt[maxn*40];
    
    int cnt,root[maxn],a[maxn];

    用不用建树??

    这个题不用,因为原来的树是空的

     

    两个线段树相减用不用再搞个树

    不用,可以在询问的时候边递归边减,

    再就是以上的问题呢,也不是绝对的,主要是得跟着题走,但基本不变的是 树的储存,insert操作,insert,query调用,离散化(好多都要)

    关于代码

    插入:

    Insert(左,右,上一个版本的根节点编号,当前版本根结点编号,要插入的位置)

    先给当前节点分配一段新内存

    然后把上一个版本的树的这个位置维护的东西给当前版本复制一遍

    把当前节点的编号改为这个新节点

    改变当前节点维护的值

    递归分别向左右插入

    Insert(左,右,上一个版本的左子节点编号,当前版本左子结点编号,要插入的位置)

    Insert(左,右,上一个版本的右子节点编号,当前版本右子结点编号,要插入的位置)

    void insert(int l,int r,int pre,int &now,int p)
    {
        hjt[++cnt]=hjt[pre];
        now=cnt;
        hjt[now].sum++;
        if(l==r) return ;
        int mid=(l+r)>>1;
        if(p<=mid) insert(l,mid,hjt[pre].l,hjt[now].l,p);
        else insert(mid+1,r,hjt[pre].r,hjt[now].r,p);
    }

    查询:

    Query(左,右,上一个版本的根节点编号,当前版本根结点编号,第几大)

    如果到了叶子节点,找到答案

    Tem=两版本相减得到的树的左子节点有多少个数

    如果大于k,证明第k大在左子树,否则在右子树 (hjt[pre].l,hjt[now].l同步进行,同步递归)

    如果在右子树,那么我们应该找的是第k-temp大,因为左子树有k个

    int query(int l,int r,int pre,int now,int k)
    {
        if(l==r) return l;
        int mid=(l+r)>>1;
        int tem=hjt[hjt[now].l].sum-hjt[hjt[pre].l].sum;
        if(k<=tem) return query(l,mid,hjt[pre].l,hjt[now].l,k);
        else return query(mid+1,r,hjt[pre].r,hjt[now].r,k-tem);
    }

    主函数

    插入

    Insert(1,n,上一个数字的那个版本,当前数字版本,找离散后的位置也就是要插入的地方)

        for(int i=1;i<=n;i++)
        {
            insert(1,n,root[i-1],root[i],getid(a[i]));
        }

    查询

    Query(1,n,前一个版本,当前版本,第k大)-1(ve下标从0开始)

           

     printf("%d
    ",ve[query(1,n,root[l-1],root[r],k)-1]);

    完整代码:

     1 #include <bits/stdc++.h>
     2 using namespace std;
     3 #define ll long long
     4 const int mod = 1e9 + 7;
     5 const int inf = 0x3f3f3f3f;
     6 const int maxn = 200009;
     7 vector<int> ve;
     8 struct node
     9 {
    10     int l, r, sum;
    11 } hjt[maxn * 40];
    12 int cnt, root[maxn], a[maxn];
    13 
    14 int getid(int x) { return lower_bound(ve.begin(), ve.end(), x) - ve.begin() + 1; }
    15 
    16 void insert(int l, int r, int pre, int &now, int p)
    17 {
    18     hjt[++cnt] = hjt[pre];
    19     now = cnt;
    20     hjt[now].sum++;
    21     if (l == r)  return;
    22     int mid = (l + r) >> 1;
    23     if (p <= mid)  insert(l, mid, hjt[pre].l, hjt[now].l, p);
    24     else  insert(mid + 1, r, hjt[pre].r, hjt[now].r, p);
    25 }
    26 
    27 int query(int l, int r, int pre, int now, int k)
    28 {
    29     if (l == r)
    30         return l;
    31     int mid = (l + r) >> 1;
    32     int tem = hjt[hjt[now].l].sum - hjt[hjt[pre].l].sum;
    33     if (k <= tem)  return query(l, mid, hjt[pre].l, hjt[now].l, k);
    34     else  return query(mid + 1, r, hjt[pre].r, hjt[now].r, k - tem);
    35 }
    36 
    37 int main()
    38 {
    39     //  freopen("in.in","r",stdin);
    40     int n, m;
    41     scanf("%d%d", &n, &m);
    42     for (int i = 1; i <= n; i++)
    43     {
    44         scanf("%d", &a[i]);
    45         ve.push_back(a[i]);
    46     }
    47     sort(ve.begin(), ve.end());
    48     ve.erase(unique(ve.begin(), ve.end()), ve.end());
    49     for (int i = 1; i <= n; i++)
    50     {
    51         insert(1, n, root[i - 1], root[i], getid(a[i]));
    52     }
    53     int l, r, k;
    54     while (m--)
    55     {
    56         scanf("%d%d%d", &l, &r, &k);
    57         printf("%d
    ", ve[query(1, n, root[l - 1], root[r], k) - 1]);
    58     }
    59     return 0;
    60 }

    学之前呢,听着这个算法非常的高大上,非常的难啊,但是真正过掉题并且能把解决问题的思路,过程口胡出来,(最好是边念叨边写)确实是很有成就感的,而且会越来越熟练,就不存在每次写线段树都在模板上改,然后时间久了就看不懂代码了的情况(至少现在是没有)

    总结,也不是说理解了思维,过掉了模板题就是说会了这个算法了,只能说是了解了基本的过程,并且希望下次碰到心里能不慌,能撸出来是最好的,做不出来也只能说是不够熟练,谁能一口气吃个胖子呢,还是得慢慢理解代码,把他磨成自己的

    好像每一次卡题之后的ac都很是欣慰,感觉对他的理解又多了那么一点点hhhhhh

  • 相关阅读:
    asp.net基础开发中常用代码大全
    IPv6網絡開發范例
    [轉]现场:是谁在住救灾帐篷者?
    运送救灾物资路上的感人画面纪实
    DataGridView新特色、常用操作
    [ZT]定制自己的Windows CE 5.0 ARM中文模拟器
    LoadRunner参数化功能详解
    [轉]灾区那么大,王十为什么直奔遵道镇,不去别处?
    [理想?夢想?]ERP项目怎么管
    乞讨老人为地震灾区捐款105元(图)
  • 原文地址:https://www.cnblogs.com/YangKun-/p/12772849.html
Copyright © 2020-2023  润新知