美文网首页
静态主席树

静态主席树

作者: Gitfan | 来源:发表于2017-08-10 00:26 被阅读0次

主席树的作用是寻找一个序列的某个区间的第k大(小)
如:给出一个序列a1,a2...an,有若干个询问,每个询问形如(l,r,k),询问l到r的第k大是多少
以下内容转载自这里
主席树的每个节点对应一颗线段树,此处有点抽象。在我们的印象中,每个线段树的节点维护的树左右子树下标以及当前节点对应区间的信息(信息视具体问题定)。对于一个待处理的序列a[1]、a[2]…a[n],有n个前缀。每个前缀可以看做一棵线段树,共有n棵线段树;若不采用可持久化结构,带来的严重后果就是会MLE,即对内存来说很难承受。根据可持久化数据结构的定义,由于相邻线段树即前缀的公共部分很多,可以充分利用,达到优化目的,同时每棵线段树还是保留所有的叶节点只是较之前共用了很多共用节点。主席树很重要的操作就是如何寻找公用的节点信息,这些可能可能出现在根节点也可能出现在叶节点。

下面是某大牛的理解:所谓主席树呢,就是对原来的数列[1..n]的每一个前缀[1..i](1≤i≤n)建立一棵线段树,线段树的每一个节点存某个前缀[1..i]中属于区间[L..R]的数一共有多少个(比如根节点是[1..n],一共i个数,sum[root] = i;根节点的左儿子是[1..(L+R)/2],若不大于(L+R)/2的数有x个,那么sum[root.left] = x)。若要查找[i..j]中第k大数时,设某结点x,那么x.sum[j] - x.sum[i - 1]就是[i..j]中在结点x内的数字总数。而对每一个前缀都建一棵树,会MLE,观察到每个[1..i]和[1..i-1]只有一条路是不一样的,那么其他的结点只要用回前一棵树的结点即可,时空复杂度为O(nlogn)。

主席树的节点存的当前节点的左孩子,右孩子以及当前序列位于区间l,r的个数。比如,现在是第j颗前缀树,即现在的序列为a1,a2,a3...aj,假设现在位于j前缀树的节点为rt,rt代表的区间为l,r,那么rt中存储的sum值是序列a1,a2...aj位于[ l ,r ]的个数。

我自己对主席树的理解,是一个线段树在修改一个值的时候,它只要修改logn个节点就可以了,那么我们只要每次增加logn个节点就可以记录它原来的状态了, 即你在更新一个值的时候仅仅只是更新了一条链,其他的节点都相同,即达到共用。由于主席树每棵节点保存的是一颗线段树,维护的区间相同,结构相同,保存的信息不同,因此具有了加减性。(这是主席树关键所在,当除笔者理解了很久很久,才相通的),所以在求区间的时候,若要处区间[l, r], 只需要处理rt[r] - rt[l-1]就可以了,(rt[l-1]处理的是[1,l-1]的数,rt[r]处理的是[1,r]的数,相减即为[l, r]这个区间里面的数。
比如说(以区间第k大为例hdu2665题目戳这里http://acm.hdu.edu.cn/showproblem.php?pid=2665):
设n = 4,q= 1;
4个数分别为4, 1, 3 ,2;
ql = 1, qr = 3, k = 2;
1.建树
首先需要建立一棵空的线段树,也是最原始的主席树,此时主席树只含一个空节点,此时设根节点为rt[0],表示刚开始的初值状态,然后依次对原序列按某种顺序更新,即将原序列加入到对应位置。此过程与线段树一样,时间复杂度为O(nlogn),空间复杂度O(nlog(n))(笔者目前没有完全搞清究竟是多少, 不过保守情况下,线段树不会超过4*n)


2.更新
我们知道,更新一个叶节点只会影响根节点到该叶节点的一条路径,故只需修改该路径上的信息即可。每个主席树的节点即每棵线段树的结构完全相同,只是对应信息(可以理解为线段树的结构完全一样,只是对应叶子节点取值不同,从而有些节点的信息不同,本质是节点不同),此时可以利用历史状态,即利用相邻的上一棵线段树的信息。相邻两颗线段树只有当前待处理的元素不同,其余位置完全一样。因此,如果待处理的元素进入线段树的左子树的话,右子树是完全一样的,可以共用,即直接让当前线段树节点的右子树指向相邻的上一棵线段树的右子树;若进入右子树,情况可以类比。此过程容易推出时间复杂度为O(logn),空间复杂度为 O(logn)。如图:

3.查询
我们先来看一个简单的线段树查询:
假设有一个序列a1,a2...an,我们要查询的是整个序列中第k大。
一个简单的解法是利用线段树:
假如线段树的节点rt,rt代表的区间为l,r,线段树节点存储的是值是一个序列位于l,r的个数。
假设序列:5 1 2 3 3 2 1 9 8 10,
排序后为 1 1 2 2 3 3 5 8 9 10

第k大为: 1 2 3 4 5 6 7 8 9 10(这里代表k)
    1 1 2 2 3 3 5 8 9 10
建好的线段树如下:
旁边的数字为节点代表的区间[ l , r ],圈圈的数字代表序列在[ l , r ]的个数

线段树.png

那么查询的代码如下:

int query(int l,int r,int rt,int k)
{
    if(l==r) return l;//or return r
    int mid=(l+r)/2;
//如果位于区间l,r的序列数>=k,那么在左子树寻找第k大
    if(sum[leftson[rt]]>=k) return query(l,mid,leftson[rt],k);
//否则,在右子树寻找第k-sum[leftson[rt]]大
    else return query(mid+1,r,rightson[rt],k-sum[leftson[rt]]);
}

假设我现在要找第5大和第8大,那么寻找路径如下:

寻找路径.png

其实,主席树的寻找与此类似,假设寻找区间为L,R,那么把第R颗前缀树和第L颗前缀树相减得到一颗线段树,再按照线段树的查找方法就可以了!

先附上处理好之后的主席树, 如图:



是不是看着很晕。。。。。。笔者其实也晕了,我们把共用的节点拆开来,看下图:



啊, 这下清爽多了,一眼看下去就知道每个节点维护的是哪棵线段树了,TAT,如果早就这样写估计很快就明白了,rt[i]表示处理完前i个数之后所形成的线段树,即具有了前缀和的性质,那么rt[r] - rt[l-1]即表示处理的[l, r]区间喽。当要查询区间[1,3]的时候,我们只要将rt[3] 和 rt[0]节点相减即可得到。如图:

这样我们得到区间[l, r]的数要查询第k大便很容易了,设左节点中存的个数为cnt,当k<=cnt时,我们直接查询左儿子中第k小的数即可,如果k>cnt,我们只要去查右儿子中第k-cnt小的数即可,这边是一道很简单的线段树了。就如查找[1, 3]的第2小数(图上为了方便,重新给节点标号),从根节点1向下搜,发现左儿子2的个数为1,1<2,所有去右儿子3中搜第2-1级第1小的数,然后再往下搜,发现左儿子6便可以了,此时已经搜到底端,所以直接返回节点6维护的值3即可就可以了。

询问区间第k大的模板

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int maxn = 100010;
int ls[maxn*30], rs[maxn*30], sum[maxn*30];//不知道开多少,开30倍应该够
int T[maxn], tot;
//T[i]为第i颗前缀树的根节点所在的位置
//ls[rt]为rt节点的左孩子所在的位置
//rs[rt]为rt节点的右孩子所在的位置
//假设rt是第j颗前缀树的某个节点,代表的区间为L-R
//那么sum[rt]表示前j个序列中数字,位于区间L-R的数字的个数

//build(1, cnt, T[0]);
void build(int l, int r, int& rt)//建立一颗空树
{
    rt = ++tot;
    sum[rt] = 0;
    if(l == r)
        return;
    int m = l+r>>1;
    build(l, m, ls[rt]);
    build(m+1, r, rs[rt]);
}

/*
    for(int i = 1; i <= n; i++)
        update(T[i-1], a[i], 1, cnt, T[i]);
*/
void update(int last, int p, int l, int r, int& rt)
{//新建一颗前缀树,last为上一颗前缀树的节点的在数组的位置,rt为新建的前缀树的节点在数组的位置
    //p为插入的数值,l r为插入区间
    
    rt = ++tot;//为新前缀树创建新节点
    
    //不管三七二十一,新的树的两个孩子都接到上一个前缀树对应的两个孩子那里
    //后面那里会有更新的分支,要么更新左孩子,要么更新右孩子,然后ls[rt]或者rs[rt]会新创建一个节点
    ls[rt] = ls[last];
    rs[rt] = rs[last];
    
    //因为新树比上一颗前缀树添加了一个数字,所以sum[rt]=sum[last]+1
//因为递归的时候保证了包含p的区间才有可能出现在这里,sum[rt]=sum[last]+1是准确的
    sum[rt] = sum[last]+1;
    
    //叶子节点的sum值都已经更新了,那肯定要return啦
    if(l == r)
        return;
        
    int m = l+r>>1;
    if(p <= m)
        update(ls[last], p, l, m, ls[rt]);//看这里,如果选择的是左边,那么ls[rt]会重新创建
    else
        update(rs[last], p, m+1, r, rs[rt]);//同理,rs[rt]也会重新创建
}

/*
int x, y, k;
printf("%d\n", num[query(T[x-1], T[y], 1, cnt, k)]);
*/
int query(int x, int y, int l, int r, int k)
{
    if(l == r)
        return l;
    int m = l+r>>1;
    int cnt = sum[ls[y]]-sum[ls[x]];
    if(k <= cnt)
        return query(ls[x], ls[y], l, m, k);//左边第k大
    else
        return query(rs[x], rs[y], m+1, r, k-cnt);//右边第k-cnt大
}
int num[maxn], a[maxn], n, q;
void cal()
{
    for(int i = 1; i <= n; i++)
    {
        scanf("%d", &a[i]);
        num[i] = a[i];
    }
    tot = 0;
    sort(num+1, num+n+1);
//    for(int i=1;i<=n;i++)
//        printf("%d ",num[i]);
//    printf("\n");
    int cnt = unique(num+1, num+n+1)-(num+1);//不重复的数字个数
//    for(int i=1;i<=cnt;i++)
//        printf("%d ",num[i]);
//    printf("\n");
    build(1, cnt, T[0]);//建一颗空树
    for(int i = 1; i <= n; i++)
    {
        a[i] = lower_bound(num+1, num+cnt+1, a[i])-num;
        //a[i]为离散化后的数值
       // printf("%d ",a[i]);
    }
   // printf("\n");
    for(int i = 1; i <= n; i++)
        update(T[i-1], a[i], 1, cnt, T[i]);
    while(q--)
    {
        int x, y, k;
        scanf("%d %d %d", &x, &y, &k);
        printf("%d\n", num[query(T[x-1], T[y], 1, cnt, k)]);
    }
}
int main()
{
        while(scanf("%d %d", &n, &q)!=EOF)
        cal();
    return 0;
}
/*
9 4
1 3 6 1 6 3 11 9 16
*/

这里我再给出在一个序列寻找第k大的代码,用线段树实现,可以比较一下与主席树的不同

#include <cstdio>
#include <cstring>
#include <algorithm>
#define mid ((l+r)>>1)
using namespace std;
const int MAXN = 100010;
int sum[MAXN<<2],num[MAXN],arr[MAXN];
void build(int l,int r,int rt)
{
    sum[rt]=0;
    if(l==r) return;
    build(l,mid,rt<<1);
    build(mid+1,r,rt<<1|1);
}
void update(int p,int l,int r,int rt)
{
    sum[rt]++;//沿途经过的节点加1
    if(l==r) return;
    if(p<=mid) update(p,l,mid,rt<<1);
    else update(p,mid+1,r,rt<<1|1);
}
//void update(int p,int l,int r,int rt)
//{
//    if(l==r)
//    {
//        sum[rt]++;
//        return;
//    }
//    if(p<=mid) update(p,l,mid,rt<<1);
//    else update(p,mid+1,r,rt<<1|1);
//    sum[rt]=sum[rt<<1]+sum[rt<<1|1];
//}
int query(int rt,int l,int r,int k)
{
    if(l==r) return l;
    int cnt=sum[rt<<1];
    if(cnt>=k) return query(rt<<1,l,mid,k);//左边查找第k大
    else return query(rt<<1|1,mid+1,r,k-cnt);//右边查找第k-cnt大
}
int main()
{
    int n,m,k,t;
    scanf("%d",&t);
    while(t--)
    {
        scanf("%d%d",&n,&m);
        build(1,n,1);
        for(int i=1;i<=n;i++)
        {
            scanf("%d",&arr[i]);
            num[i]=arr[i];
        }
        sort(num+1,num+1+n);
        int cnt=unique(num+1,num+1+n)-(num+1);
        for(int i=1;i<=n;i++)
        {
            arr[i]=lower_bound(num+1,num+1+cnt,arr[i])-num;//arr[i]离散化
        }
        for(int i=1;i<=n;i++)
        {
            update(arr[i],1,cnt,1);
        }
        while(m--)
        {
            scanf("%d",&k);
            printf("%d\n",num[query(1,1,cnt,k)]);
        }
    }
}

K-th Number
题意:
询问区间第k大

#include <cstdio>
#include <cstring>
#include <algorithm>
#define mid ((l+r)>>1)
using namespace std;
const int MAXN = 100010*40;
int root[MAXN],ls[MAXN],rs[MAXN],sum[MAXN];
int tot;
void build(int l,int r,int &rt)
{
    rt=++tot;
    sum[rt]=0;
    if(l==r) return;
    build(l,mid,ls[rt]);
    build(mid+1,r,rs[rt]);
}
void update(int last,int p,int l,int r,int &rt)
{
    rt=++tot;
    ls[rt]=ls[last];
    rs[rt]=rs[last];
    sum[rt]=sum[last]+1;
    if(l==r) return;
    if(p<=mid) update(ls[last],p,l,mid,ls[rt]);
    else update(rs[last],p,mid+1,r,rs[rt]);
}
int query(int rtx,int rty,int l,int r,int k)
{
    if(l==r) return l;
    int cnt=sum[ls[rty]]-sum[ls[rtx]];
    if(cnt>=k) return query(ls[rtx],ls[rty],l,mid,k);
    else return query(rs[rtx],rs[rty],mid+1,r,k-cnt);
}
int arr[MAXN],num[MAXN];
int main()
{
    int n,m,lef,rig,k;
    while(scanf("%d%d",&n,&m)!=EOF)
    {
        tot=0;
        for(int i=1;i<=n;i++)
        {
            scanf("%d",&arr[i]);
            num[i]=arr[i];
        }
        sort(num+1,num+1+n);
        int cnt=unique(num+1,num+1+n)-(num+1);
        build(1,cnt,root[0]);
        for(int i=1;i<=n;i++)
        {
            arr[i]=lower_bound(num+1,num+1+cnt,arr[i])-num;
        }
        for(int i=1;i<=n;i++) update(root[i-1],arr[i],1,cnt,root[i]);
        while(m--)
        {
            scanf("%d%d%d",&lef,&rig,&k);
            printf("%d\n",num[query(root[lef-1],root[rig],1,cnt,k)]);
        }
    }
}

相关文章

  • 静态主席树

    主席树的作用是寻找一个序列的某个区间的第k大(小)如:给出一个序列a1,a2...an,有若干个询问,每个询问形如...

  • 静态主席树

  • 可持久化线段树

    题目链接:可持久化数组 可持久化数组: 题目链接:Kth number 静态主席树: 题目链接:Dynamic R...

  • 主席树

    主席树当然是很厉害的呀【BZOJ 1901】 Zju2112 Dynamic Rankings各种线段树https...

  • 主席树(静态) 图文讲解让你一次就懂 hdu2665为例

    主席树 先介绍一下主席树,主席树也称函数式线段树也称可持久化线段树。(其实就是支持查询历史版本,这个在看完之后就会...

  • 可修改主席树

    普通主席树 普通主席树比较简单 就是很多个权值线段树 每一次加进去log个节点(每层一个),剩下的节点用原来的线段...

  • github io

    静态网页。 红黑树,B树 太平天国农民起义。

  • 二叉搜索树、平衡二叉树

    -二叉搜索树 查找问题:静态查找和动态查找,静态查找可以用二分查找-判定树,那么针对动态查找数据如何组织?(树的动...

  • 树的遍历

    树的静态写法 树的遍历 1079 Total Sales of Supply Chain 1090 Highest...

  • 树的静态写法

    这里的树是指一般意义上的树,即子结点个数不限且子结点没有先后次序的树,而不是上文讨论的二叉树。 struct no...

网友评论

      本文标题:静态主席树

      本文链接:https://www.haomeiwen.com/subject/ctaqrxtx.html