蒟蒻林荫的小复习——主席树

主席树也就是指可持久化线段树,大致思想也就是每次利用之前的重复信息,只为被更新的一部分开辟新点。而且所谓可持久化线段树实际上是指可持久化权值线段树,线段树中每个端点存的是这个端点所代表的树的出现次数。

而在主席树的维护当中对于每个历史版本如果都开一颗新树,那么M将是最终的结局。正确解法则是为每一个改变的点分配编号而未改变的点直接链接到新树上。

下面就来从主席树的两个经典问题来考虑

1:区间第K小问题 传送门

首先分析问题,给出一个序列,每次给定一个封闭区间,求这个封闭区间以内的最小值。

上面我们说过,主席树实际上是一颗权值线段树,点里面存的是点所代表这个数出现了多少次。先看数据范围,离散化是跑不了的。反正n<=2*10^5

那么就把原数列离散化到1---2*10^5的区间之内。开n棵树,第i颗代表压入前i个数之后各个数字的出现情况。

声明一个3元组,分别代表左儿子编号,右儿子编号,当前节点子树内所有元素共计出现次数(如果是叶子节点就代表叶子节点代表的这个元素出现次数)

所以说动态建树不用解释了吧

int build(int l,int r)
{
    int rt=++tot;
    if(l<r)
    {
        L[rt]=build(l,mid);//每个节点构造线段树中左子树位置
        R[rt]=build(mid+1,r);//每个节点构造线段树中右子树位置
    }
    return rt;
}

build(1,m)m为不同的元素个数。

下面就到了插入了。很明显,插入一定是在上个版本的基础上进行的。那么就先让新版本的左右子树都等于旧版本的,但是新版本根节点元素个数=老版本的+1。然后再判断新插入的数的左右区间隶属,因为已经进行了离散化,所以新插入的数会按照大小分给左右区间。叶子节点所表示的区间即为x的值

int update(int root,int l,int r,int x)
{//x即为离散化后的带插入数

//代码前面有#define mid (l+r)/2
    int rt=++tot;
    L[rt]=L[root];
    R[rt]=R[root];
    sum[rt]=sum[root]+1;
    if(l<r)
    {
        if(x<=mid)
        {
            L[rt]=update(L[root],l,mid,x);

        }
        else
        {
            R[rt]=update(R[root],mid+1,r,x);
        }
    }
    return rt;
}

下面就是查询了,选择L-1版和R版,每一次求得sum[L[now]]-sum[L[last]]如果该结果>=k,则证明第K小的值在当前子树的左子树中,否则在右子树中。

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

然后就这样的写完了,还有一点就是离散化部分需要注意一下。

// luogu-judger-enable-o2
#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
#define ll long long int
#define mid (l+r)/2
const int N=200001;
bool function(int a,int b)
{
    return a<b;
}
const int LOG=20;
int n,m,q,tot=0;
int a[N],b[N];
int T[N],sum[N*LOG],L[N*LOG],R[N*LOG];
int build(int l,int r)
{
    int rt=++tot;
    if(l<r)
    {
        L[rt]=build(l,mid);//每个节点构造线段树中左子树位置
        R[rt]=build(mid+1,r);//每个节点构造线段树中右子树位置
    }
    return rt;
}
int update(int root,int l,int r,int x)
{
    int rt=++tot;
    L[rt]=L[root];
    R[rt]=R[root];
    sum[rt]=sum[root]+1;
    if(l<r)
    {
        if(x<=mid)
        {
            L[rt]=update(L[root],l,mid,x);

        }
        else
        {
            R[rt]=update(R[root],mid+1,r,x);
        }
    }
    return rt;
}
int query(int u,int v,int l,int r,int k)
{
    if(l==r)
    {
        return l;
    }
    int x=sum[L[v]]-sum[L[u]];
    if(x>=k)
    {
        return query(L[u],L[v],l,mid,k);
    }
    else
    {
        return query(R[u],R[v],mid+1,r,k-x);
    }
}
int main()
{
    scanf("%d%d",&n,&q);
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&a[i]);
        b[i]=a[i];
    }
    sort(b+1,b+1+n,function);
    m=unique(b+1,b+1+n)-b-1;
    T[0]=build(1,m);
    for(int i=1;i<=n;i++)
    {
        a[i]=lower_bound(b+1,b+1+m,a[i])-b;
        T[i]=update(T[i-1],1,m,a[i]);
    }
    while(q--)
    {
            int x, y, z;
            scanf("%d%d%d", &x, &y, &z);
            int p = query(T[x-1], T[y], 1, m, z);
            printf("%d\n", b[p]);
    }
    return 0;
}

AC

2:长得很像普通线段树的可持久化线段树

这种题就没有必要写权值线段树了,毕竟权值线段树写起来很难受。

然而说实话这道题挺水,真的就是线段树的可持久化,直接上代码吧

#include<iostream>
#include<cstdio>
using namespace std;
struct POINT
{
    int ls,rs,maxx;
};
POINT t[100001*16];
int n,m,tot=1;
int a[10001];
int Root[100001];
void pushup(int x)
{
    t[x].maxx=max(t[t[x].ls].maxx,t[t[x].rs].maxx);
}
void build(int rt,int l,int r)
{
    if(l==r)
    {
        t[rt].maxx=a[l];
        return;
    }
    t[rt].ls=++tot;
    t[rt].rs=++tot;
    int mid=(l+r)>>1;
    build(t[rt].ls,l,mid);
    build(t[rt].rs,mid+1,r);
    pushup(rt);
}
void CHANGE(int rt1,int rt2,int l,int r,int pos,int ke)
{
    if(l==r)
    {
        t[rt1].maxx=ke;
        return ;
    }
    int mid=(l+r)>>1;
    if(pos<=mid)
    {
        t[rt1].rs=t[rt2].rs;
        t[rt1].ls=++tot;
        CHANGE(t[rt1].ls,t[rt2].ls,l,mid,pos,ke);
    }
    else
    {
        t[rt1].ls=t[rt2].ls;
        t[rt1].rs=++tot;
        CHANGE(t[rt1].rs,t[rt2].rs,mid+1,r,pos,ke);
    }
    pushup(rt1);
}
int QUERY(int rt,int l,int r,int nl,int nr)
{
    if(nl<=l&&r<=nr)
    {
        return t[rt].maxx;
    }
    if(l==r)
    {
        return t[rt].maxx;
    }
    int sed=-998244353;
    int mid=(l+r)>>1;
    if(nl<=mid)
    {
        //cout<<t[rt].ls<<‘ ‘<<l<<‘ ‘<<mid<<endl;
        sed=max(sed,QUERY(t[rt].ls,l,mid,nl,nr));
    }
    if(nr>mid)
    {
        //cout<<t[rt].rs<<‘ ‘<<mid+1<<‘ ‘<<r<<endl;
        sed=max(sed,QUERY(t[rt].rs,mid+1,r,nl,nr));
    }
    return sed;
}
void PDDFS(int x)
{
    if(x==0)
        return;
    cout<<x<<endl;
    PDDFS(t[x].ls);
    PDDFS(t[x].rs);
}
int main()
{
    freopen("longterm_segtree.in","r",stdin);
    freopen("longterm_segtree.out","w",stdout);
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&a[i]);
    }
    build(1,1,n);
    int a1,a2,a3,a4;
    Root[1]=1;
    int cnt=1;
    while(m--)
    {
        scanf("%d%d%d%d",&a1,&a2,&a3,&a4);
        if(a1==0)
        {    //PDDFS(Root[a2]);
            cout<<QUERY(Root[a2],1,n,a3,a4)<<endl;

        }
        else
        {
            Root[++cnt]=++tot;
            CHANGE(Root[cnt],Root[a2],1,n,a3,a4);
        }
    }
    return 0;
}

完结撒花!

原文地址:https://www.cnblogs.com/XLINYIN/p/11355673.html

时间: 2024-10-06 00:29:40

蒟蒻林荫的小复习——主席树的相关文章

蒟蒻林荫小复习——关于有限制区间元素查询的一些解法

如题:本文主要说明对于区间有限制查询的一些解法(其实就两种) 问题1:给定一个数列,要求查询区间L—R中所有大于等于Va小于等于Vb的元素和 解法: 1.线段树套权值线段树 第一维维护区间,第二维作为权值线段树,维护值域在A—B之间的元素之和 每次查询就从第一维拉到对应区间,然后用Va和Vb确定在权值线段树中的查询范围即可 2.分块 分块数组记为a,对每一个a块都开一个数组b,b数组将a块中元素拷贝后排序,新建c,对于每一个b都求前缀和 这样对于整块而言,用二分确定Va和Vb在b数组中的位置Ia

spoj COT - Count on a tree (树上第K小 LCA+主席树)

链接: https://www.spoj.com/problems/COT/en/ 思路: 首先看到求两点之前的第k小很容易想到用主席树去写,但是主席树处理的是线性结构,而这道题要求的是树形结构,我们可以用dfs跑出所有点离根的距离-dep[i](根为1,dep[1]也为1)在dfs的过程 中,我们对每一个节点建一棵线段树,那么[a,b]就是:root[a] + root[b] - root[lca(a,b)] - root[f[lca(a,b)]]; (因为a-b的路径上的权值还要算上lca(

[POJ2761]Feed the dogs(静态区间第k小,主席树)

题目链接:http://poj.org/problem?id=2761 题意:如题 主席树只能用模版,好菜. 1 /* 2 ━━━━━┒ギリギリ♂ eye! 3 ┓┏┓┏┓┃キリキリ♂ mind! 4 ┛┗┛┗┛┃\○/ 5 ┓┏┓┏┓┃ / 6 ┛┗┛┗┛┃ノ) 7 ┓┏┓┏┓┃ 8 ┛┗┛┗┛┃ 9 ┓┏┓┏┓┃ 10 ┛┗┛┗┛┃ 11 ┓┏┓┏┓┃ 12 ┛┗┛┗┛┃ 13 ┓┏┓┏┓┃ 14 ┃┃┃┃┃┃ 15 ┻┻┻┻┻┻ 16 */ 17 #include <algorithm>

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[]为原

蒟蒻林荫小复习——K短路的A*解法

看标题都知道讲的是什么,但为什么特指是A*关于K短路的解法呢? 因为林荫不会其他的. 能看到这篇博客的估计也都知道K短路和A*分别是什么了吧,下面只介绍一下估价函数 由于林荫并没有经过学术训练,所以一下关于A*的理解均为感性,仅可作为OIer速成知识点时的一点资料, 切莫作为算法学术依据. 先思考一下,对于任意一条K短路,是不是均可写成由一部分最短路和一部分其他路径组成,而且这两部分路径还有且仅有一个公共点. 说明:1.任何一条路径都可以视为由其他到终点的路径和终点到终点的最短路组成 2.哪怕前

蒟蒻林荫小复习——带权并查集

实际上很早之前林荫是有这个技能的.(废话!要不直接叫小学习好了) 众所周知,并查集可以用来维护一些元素之间相连的关系(不知道的出门右转幼儿园) 而状态压缩可以使得并查集查询一对元素的关系的速度变快(O1) 状态压缩之后的并查集实际上是一个由fa数组(相当于单向链表)构成的菊花图 那么,如何用并查集来维护元素之间实际的数量关系呢? 先看一个例子: 假定现在有3个小朋友ABC,B比A大3岁,C比B大2岁.对于这个问题,如果用不带状压的并查集进行表示,那么就可以得到一条链.B到A的边权3表示 B比A大

蒟蒻的省选复习————连载中

1.筛法 1 var 2 n,m,i,x,tot,j :longint; 3 prime :array[0..10000050] of boolean; 4 p :array[0..10000050] of longint; 5 begin 6 read(n,m); 7 for i:=2 to n do 8 begin 9 if not prime[i] then 10 begin 11 inc(tot); 12 p[tot]:=i; 13 end; 14 for j:=1 to tot do

蒟蒻之省选复习——————连载中

1.筛法 1 var 2 n,m,i,x,tot,j :longint; 3 prime :array[0..10000050] of boolean; 4 p :array[0..10000050] of longint; 5 begin 6 read(n,m); 7 for i:=2 to n do 8 begin 9 if not prime[i] then 10 begin 11 inc(tot); 12 p[tot]:=i; 13 end; 14 for j:=1 to tot do

少年,想学带修改主席树吗 | BZOJ1901 带修改区间第k小

少年,想学带修改主席树吗 | BZOJ1901 带修改区间第k小 有一道题(BZOJ 1901)是这样的:n个数,m个询问,询问有两种:修改某个数/询问区间第k小. 不带修改的区间第k小用主席树很好写,不会的同学可以看一下这个. 加上修改怎么做呢?我们可以用数学老师成天讲的类比思想: 可以发现,不修改的区间k小问题中,每加入一个原序列中的数,对应的主席树在上一个的基础上进行修改,而查询的时候用右端点主席树减去左端点左边的主席树.这样的操作就像是维护前缀和:每次加入一个元素的时候,sum[i] =