主席树学习笔记

Posted wlzs1432

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了主席树学习笔记相关的知识,希望对你有一定的参考价值。

Part I 静态主席树

定义

主席树最基础可以维护区间K大的问题,由于其本质是可持久化线段树,所以要对线段树有很深的理解。

栗子:区间第K小

给定N个正整数构成的序列,将对于指定的闭区间查询其区间内的第K小值。

首先这种处理区间的问题肯定要想到区间数据结构。显然如果是指定了区间,可以把读入的数据离散化,然后建一颗值域线段树。

但是要在任意的[l,r]中查询第k小,一些大神就想到了前缀和。

首先建N颗线段树,第i棵维护区间a1-ai的每个数的出现个数。

此时值域线段树的结构都要保证完全相等,这样这些线段树就具有了可减性,就可以用前缀和维护了。

那这样就要建N棵线段树,空间无法承受。

我们可以轻易发现,维护a1-ai和a1-ai+1的线段树,的每一个非叶节点的子树有一半的结构都是相等的,如果能够只修改一半,空间的问题就会解决。

那如何处理空间了

比如说对于一个数1926,817,1989,604,首先离散化

于是 1926 就等价于3,以此类推。

那按照刚才的思路,我们可以先建一棵树维护a1-a1

如下图

 

 

 圆圈中的数字代表线段树维护的东西,也就是在这个区间内的数有多少个。

这个时候我们考虑建a1-a2的线段树,如果重新建这棵树会变成这样

 

对比前一棵树,我们发现几乎一半的结构都是相同的。(圈出来的即为相同)

那我们每次先新建一个根节点:

 

 其中维护a1-ai的根节点为rooti,如下图

我们每一棵节点都要记录他左右儿子的编号(后面会解释)。

这样我们可以用让root2的右儿子的编号等于root1右儿子的编号相同的方式使root2的右儿子等于root1的左儿子且节约空间

个人感觉有点像链表:

这样要修改的节点就大大减少了。

至于为什么要记录每个节点的左右儿子编号,正是因为主席树有一半的结构来源与前一棵树,这是这个节点与先前那个节点不构成线段树中i*2和i*2+1的关系

对于查询,我们要利用到前缀和:

 对于查询,首先对于这棵子树查询K大:

我们只需要关注这棵子树的左子树出现的数的个数总和与K的大小关系:

如果小的话:在左子树查K小

如果大的话:往右子树查(k-左子树总和)小

那怎么知道L-R的个数:

前缀和,首先结构相同自然可以直接减啦!

至此静态主席树求静态第K小就搞定了,下面结合代码(非常丑陋):

#include<bits/stdc++.h>

using namespace std;

const int MAXN = 2 * 100000 + 10;

inline int read()
{
    int f = 1 ,x = 0;
    char ch;
    do
    {
        ch =getchar();
        if(ch==\'-\') f = -1;
    }while(ch<\'0\'||ch>\'9\');
    do
    {
        x=(x<<3)+(x<<1)+ch-\'0\';
        ch = getchar();
    }while(ch>=\'0\'&&ch<=\'9\');
    return f*x;
}

int n,m;

struct node
{
    int val;
    int id;
    friend bool operator < (node a1,node a2)
    {
        return a1.val<a2.val;    
    } 
};

node a[MAXN];

int c[MAXN];

struct Tree
{
    int lc;
    int rc;
    int sum;
    Tree()
    {
        sum = 0;    
    } 
};

Tree tree[MAXN*20];

int root[MAXN];

int cnt =0;

inline void init()
{
    root[0]=0;
    tree[0].lc=tree[0].rc=0;
    tree[0].sum=0;
    return;
}

inline void update(int num,int &rt ,int l,int r)
{
    tree[++cnt]=tree[rt];
    rt = cnt;
    tree[rt].sum++;
    if(l==r) return;
    int mid = (l+r)>>1;
    if(num<=mid) update(num,tree[rt].lc,l,mid);
    else update(num,tree[rt].rc,mid+1,r);
}

inline int query(int l,int r,int k,int x,int y)
{
    int now = tree[tree[r].lc].sum-tree[tree[l].lc].sum;
    if(x==y) return x;
    else
    {
        int mid = (x+y)>>1;
        if(now>=k)
        {
            return query(tree[l].lc,tree[r].lc,k,x,mid);    
        } 
        else return query(tree[l].rc,tree[r].rc,k-now,mid+1,y);
    } 
}

int main()
{
    n = read();
    m = read();
    
    for(int i=1;i<=n;i++) a[i].val=read(),a[i].id = i;
    
    sort(a+1,a+n+1);
    
    for(int i=1;i<=n;i++)
    {
        c[a[i].id]=i;
    }
    
    init();
    
    for(int i=1;i<=n;i++)
    {
        root[i]=root[i-1];
        update(c[i],root[i],1,n);
    }
    
    for(int i=1;i<=m;i++)
    {
        int L =read();
        int R = read();
        int k=read();
        printf("%d\\n",a[query(root[L-1],root[R],k,1,n)].val);
    }
}

栗子:可持久化数组

你需要维护这样的一个长度为 N N 的数组,支持如下几种操作

1.在某个历史版本上修改某一个位置上的值

2.访问某个历史版本上的某一位置的值

可持久化数组可以用可持久化线段树实现。

其实就是每次在对应的历史版本把节点复制,然后修改。

丑陋的代码:

#include<bits/stdc++.h>

using namespace std;

const int MAXN = 1000001;

inline int read()
{
    char ch;
    int fl=1;
    int x=0;
    do{
      ch= getchar();
      if(ch==\'-\')
        fl=-1;
    }while(ch<\'0\'||ch>\'9\');
    do{
        x=(x<<3)+(x<<1)+ch-\'0\';
        ch=getchar();
    }while(ch>=\'0\'&&ch<=\'9\');
    return x*fl;
}

int n,m;

int a[MAXN];

struct node
{
    int lc;
    int rc;
    int sum;
};

node tree[MAXN*20];

int root[MAXN];

int cnt=0;

int tt = 0;

int cc[MAXN],ccnt=0;

inline void build(int& rt,int l,int r)
{
    ++cnt;
    rt = cnt;
    if(l==r) {
        tree[rt].sum = a[l];
        return;        
    }
    int  mid = (l+r)>>1;
    build(tree[rt].lc,l,mid);
    build(tree[rt].rc,mid+1,r);
}

inline void update(int& rt,int l,int r,int x,int y)
{
     ++cnt;
     tree[cnt] = tree[rt];
     rt = cnt;
    if(l==r)
    {
        tree[rt].sum = y;
        return ;
    }
    int mid = (l+r)>>1;
    if(x<=mid)
    {
        update(tree[rt].lc,l,mid,x,y);
    }
    else
    {
        update(tree[rt].rc,mid+1,r,x,y);
    }
}

inline int query(int rt,int l,int r,int x)
{
    if(l==r)
    {
        return tree[rt].sum;
    }
    else
    {
        int mid = (l+r)>>1;
        if(x<=mid)
        {
            return query(tree[rt].lc,l,mid,x);
        }
        else
        {
             return query(tree[rt].rc,mid+1,r,x);
        }
    }
}

int main()
{
    n = read(),m = read();
    for(int i=1;i<=n;i++) a[i]=read();
    build(root[0],1,n);
    cc[0]=root[0];
    for(int i=1;i<=m;i++)
    {
        int bb = read();
        int opt = read();
        if(opt==1)
        {
            int x = read();
            int y = read();
            ++tt;
            cc[tt]=cc[bb];
            root[tt]=cc[tt];
            update(root[tt],1,n,x,y);
            cc[tt]=root[tt];
        }
        if(opt==2)
        {
            ++tt;
            cc[tt]=cc[bb];
            root[tt]=cc[tt];
            int x = read();
            printf("%d\\n",query(cc[tt],1,n,x));
        }
    }
}

 

以上是关于主席树学习笔记的主要内容,如果未能解决你的问题,请参考以下文章

复习笔记主席树

主席树学习记录

线段树+主席树笔记

[知识学习] 主席树

主席树学习--入门

关于线段树的一些学习笔记——(无限施工中)