可持久化线段树/主席树 (P3919 + P3834)

19 篇文章 0 订阅
17 篇文章 1 订阅

建议先了解一下动态开点线段树,可以参考一下我写的这篇


什么是可持久化线段树:

若询问次数为q,维护区间为m

则所需空间为O(q*logm)

通过root[]数组,记录不同版本的根节点编号,我们可以方便的在某个版本上修改和查询

问题1:什么是不同的版本?

我在某个版本的线段树上做了某次修改,修改后的线段树,就是不同与之前的一个船新版本。

问题2:为什么只要O(q*logm)?

对于在某个版本上进行的单点修改,我们只涉及logm个节点需要修改(与动态开点线段树一样的思想)

剩下的沿用老版本的即可,因此,建立一个新版本(进行一次修改操作)只需要新建logm个节点(区间修改不会证明)

看图看图:

 


讲一下更新和询问的代码:

一、这是需要的数组和偷懒的宏定义

#define tl tree[rt].l
#define tr tree[rt].r
#define mid int m=l+r>>1
#define lson tl,l,m
#define rson tr,m+1,r
struct node
{
    int l,r;
    int sum;
}tree[M];

二、update

void update(int old,int p,int x,int& rt,int l,int r)
{
    rt = sz++;
    if (l==r){
        tree[rt].val = x;
        return ;
    }
    tree[rt] = tree[old];
    mid;
    if (p<=m) update(tl,p,x,lson);
    else      update(tr,p,x,rson);
    //这里一般都需要push_up
}

old表示老版本维护这段区间的节点编号,p表示单点修改的位置,x表示要修改的值,二分找区间,p所在的那颗子树一定是需要修改的,因此那个节点就要新开辟

即rt = sz++。

值的注意的是这个int &rt,可以方便的将上一层递归的rt的左/右儿子赋值。

以及新节点首先获得老版本的全部信息,即tree[rt] = tree[old],再去递归修改需要修改的儿子节点

主函数中

update(root[所需的老版本],p,x,root[当前新版本],1,维护的区间右界)

这样可以在更新完之后获得当前版本的根节点编号

三、query

int query(int p,int rt,int l,int r)
{
    if (l==r){
        return tree[rt].val;
    }
    mid;
    if (p<=m) return query(p,lson);
    else      return query(p,rson);
}

查询第x个版本主函数就写:

query(p,root[x],1,维护的区间右界)


例题一:

洛谷3919

代码上面给的差不多了,自己补补全,检验一下自己学会了否

代码:

#include<bits/stdc++.h>

using namespace std;

#define ll long long
#define for1(i,a,b) for (int i=a;i<=b;i++)
#define for0(i,a,b) for (int i=a;i<b;i++)
#define tl tree[rt].l
#define tr tree[rt].r
#define mid int m = l+r>>1
#define lson tl,l,m
#define rson tr,m+1,r

const int N = 1e6+5;
const int M = 1000000*20+5;

struct node
{
    int l,r;
    int val;
}tree[M];
int root[N],sz;
int a[N];

void update(int old,int p,int x,int& rt,int l,int r)
{
    rt = sz++;
    if (l==r){
        tree[rt].val = x;
        return ;
    }
    tree[rt] = tree[old];
    mid;
    if (p<=m) update(tl,p,x,lson);
    else      update(tr,p,x,rson);
}

int query(int p,int rt,int l,int r)
{
    if (l==r){
        return tree[rt].val;
    }
    mid;
    if (p<=m) return query(p,lson);
    else      return query(p,rson);
}

void build(int& rt,int l,int r)
{
    rt = sz++;
    if (l==r){
        tree[rt].val = a[l];
        return ;
    }
    mid;
    build(lson);
    build(rson);
}
/*
void display(int rt,int l,int r)
{
    if (l==r) {printf("%d ",tree[rt].val);return ;}
    mid;
    display(lson);
    display(rson);
}
*/
int main()
{
    int n,m;
    scanf("%d %d",&n,&m);
    sz = 1;
    for1(i,1,n) scanf("%d",a+i);
    build(root[0],1,n);


    int ver,op,pos,x;
    for1(i,1,m){
        scanf("%d %d %d",&ver,&op,&pos);
        if (op==1){
            scanf("%d",&x);
            update(root[ver],pos,x,root[i],1,n);
            //printf("Verson %d:",i);display(root[i],1,n);puts("");
        }
        else {
            root[i] = root[ver];
            //printf("Verson %d:",i);display(root[i],1,n);puts("");
            printf("%d\n",query(pos,root[ver],1,n));
        }
    }
    return 0;
}



例题二:

洛谷3834

这题讲一下,求静态区间第k小

方法一

先把数据离散化

主席树维护什么:区间内这些数出现的次数(比如询问[1,3]表示我们需要知道1,2,3这几个数出现了几次)

第x个版本维护:前x个数扔进树中,区间这些数出现的次数

那么询问区间[l,r]第k大

某段[x,y]区间内的数出现的次数等于版本r[x,y]这些数出现次数-版本l-1[x,y]这些数出现次数

也就是说去掉了前l-1个数的影响

总结:l,r索引版本,与我们处理哪部分区间无关

接下去二分逼近,

举个例子,我寻找[3,6]第2小,维护的区间大小为[1,1000]

我们需要用:版本6-版本2 = 获取只有3~6这几个数的线段树状态

然后判断第2小的数在左儿子还是右儿子

用sum(x,y)表示这段区间的数出现次数

if (sum(1,500)>=2)在左区间中继续找

else 查找右子树([501,1000])中第 2 - sum(1,500)小的数即可

递归重点是,区间被被逼进到l==r

代码:

#include<bits/stdc++.h>

using namespace std;

#define ll long long
#define for1(i,a,b) for (int i=a;i<=b;i++)
#define for0(i,a,b) for (int i=a;i<b;i++)
#define tl tree[rt].l
#define tr tree[rt].r
#define mid int m=l+r>>1
#define lson tl,l,m
#define rson tr,m+1,r

const int N = 2e5+5;
const int M = 200000*20+5;

int a[N];

int data[N];
void discrete(int n)//使用该函数把a[i]变成原本a[i]离散化后对应的数,data可以根据离散化后的值推实际的大小
{
    for1(i,1,n) data[i] = a[i];
    sort(data+1,data+1+n);
    int cnt = unique(data+1,data+1+n) - data;
    for1(i,1,n) a[i] = lower_bound(data+1,data+cnt,a[i]) - data;
}

struct node
{
    int l,r;
    int sum;
}tree[M];
int sz,root[N];

void push_up(int rt){
    tree[rt].sum = tree[tl].sum + tree[tr].sum;
}

void update(int old,int p,int& rt,int l,int r)
{
    rt = sz++;//这里容易脑抽写成if(!rt) rt = sz++
    if (l==r){
        tree[rt].sum = tree[old].sum + 1;
        return ;
    }
    tree[rt] = tree[old];
    mid;
    if (p<=m) update(tl,p,lson);
    else      update(tr,p,rson);
    push_up(rt);
}

int ccnt;
int query(int old,int k,int rt,int l,int r)
{
    if (l==r) return data[l];
    mid;
    ccnt = tree[tl].sum - tree[tree[old].l].sum;
    if (ccnt >= k) return query(tree[old].l,k,lson);
    else           return query(tree[old].r,k-ccnt,rson);
}

int main()
{
    int n,m;
    scanf("%d %d",&n,&m);
    sz = 1;
    for1(i,1,n) scanf("%d",a+i);
    discrete(n);
    for1(i,1,n) update(root[i-1],a[i],root[i],1,n);

    int l,r,k;
    for1(i,1,m){
        scanf("%d %d %d",&l,&r,&k);
        printf("%d\n",query(root[l-1],k,root[r],1,n));
    }

    return 0;
}

其实也可以不离散化,毕竟都动态开点了,直接逼近即可

代码:

#include<bits/stdc++.h>

using namespace std;

#define ll long long
#define for1(i,a,b) for (int i=a;i<=b;i++)
#define for0(i,a,b) for (int i=a;i<b;i++)
#define mid int m = l+r>>1
#define lson tree[rt].l,l,m
#define rson tree[rt].r,m+1,r
#define tl tree[tree[rt].l]
#define tr tree[tree[rt].r]
#define Min (-1e9)
#define Max (1e9)

namespace FastI{//快读板子,不用管
    const int SIZE = 1 << 16;
    char buf[SIZE], str[64];
    int l = SIZE, r = SIZE;
    int read(char *s) {
        while (r) {
            for (; l < r && buf[l] <= ' '; l++);
            if (l < r) break;
            l = 0, r = int(fread(buf, 1, SIZE, stdin));
        }
        int cur = 0;
        while (r) {
            for (; l < r && buf[l] > ' '; l++) s[cur++] = buf[l];
            if (l < r) break;
            l = 0, r = int(fread(buf, 1, SIZE, stdin));
        }
        s[cur] = '\0';
        return cur;
    }
    template<typename type>
    bool read(type &x, int len = 0, int cur = 0, bool flag = false) {
        if (!(len = read(str))) return false;
        if (str[cur] == '-') flag = true, cur++;
        for (x = 0; cur < len; cur++) x = x * 10 + str[cur] - '0';
        if (flag) x = -x;
        return true;
    }
    template <typename type>
    type read(int len = 0, int cur = 0, bool flag = false, type x = 0) {
        if (!(len = read(str))) return false;
        if (str[cur] == '-') flag = true, cur++;
        for (x = 0; cur < len; cur++) x = x * 10 + str[cur] - '0';
        return flag ? -x : x;
    }
} using FastI::read;

const int N = 2e5;
const int M = 32*2e5 + 5;

struct T
{
    int l,r;
    int cnt;
}tree[M];
int root[M],sz,nowv;

void push_up(int rt){
    tree[rt].cnt = tl.cnt + tr.cnt;
}

void update(int p,int old,int&rt,int l,int r){
    rt = sz++;
    tree[rt] = tree[old];
    if (l==r){
        tree[rt].cnt++;
        return ;
    }
    mid;
    if (p<=m) update(p,tree[old].l,lson);
    else update(p,tree[old].r,rson);
    push_up(rt);
}

int query(int old,int k,int rt,int l,int r){
    if (l==r) return l;
    mid;
    int d = tl.cnt - tree[tree[old].l].cnt;
    if (k<=d) return query(tree[old].l,k,lson);
    else return query(tree[old].r,k-d,rson);
}

int main()
{
    //freopen("C:/Users/DELL/Desktop/input.txt", "r", stdin);
    //freopen("C:/Users/DELL/Desktop/my.txt", "w", stdout);
    sz = nowv = 1;
    int n,q,x;
    //scanf("%d %d",&n,&q);
    read(n);read(q);
    for1(i,1,n){
        //scanf("%d",&x);
        read(x);
        update(x,root[nowv-1],root[nowv],Min,Max);//第i版本储存前i个数标记过的线段树,每次直接利用上一版本即可
        //dfs(root[nowv],Min,Max);puts("");
        nowv++;//不要合并98,100行为update(x,root[nowv-1],root[nowv++],Min,Max);你可以自己试试这样会咋样
    }

    int ql,qr,k;
    while (q--){
        //scanf("%d %d %d",&ql,&qr,&k);
        read(ql);read(qr);read(k);
        int l = Min,r = Max;
        printf("%d\n",query(root[ql-1],k,root[qr],Min,Max));
    }
    return 0;
}

 

 

 

 

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值