あいさか たいがblogAisaka_Taiga的博客
//https://img2018.cnblogs.com/blog/1646268/201908/1646268-20190806114008215-138720377.jpg

主席树学习笔记

Toretto·2023-04-12 15:10·39 次阅读

主席树学习笔记

主席树,又名可持久化线段树,可以访问多个历史版本的树上存的信息。

图及其他来源于此:https://www.cnblogs.com/hyfhaha/p/10678275.html

基本思想#

用到的基本思想就是对于每一个修改版本的树,只新建修改后的节点,如果是每一个版本新开一个线段树的话空间一定不够。

image

这是普通的线段树单点修改。

image

这是主席树的单点修改。

几个月前我被这个东西劝退了,但现在来看挺好理解的,无非就是把修改的点从下到上涉及到的值有变化的点新开一个点,然后对于一些有没有变化的点,直接和修改后的连起来即可,根据根节点的不同可以判断是那个版本的点。

具体一点说,我们如果是暴力地每修改一次就建一棵新树的话,对于数据大一点的题目就会 MLE ,所以我们根据上面的图可以发现,我们进行一次单点修改的操作时,我们实际上修改了值的点是只有 log n 个的,其余的大部分点都是没有修改直接 ctrl cv 过来的,所以就有了第二张图所呈现的,我们开新点的时候只开修改的点,这样就可以使空间的消耗大大降低,同时又能做到对于每一个的版本都进行查询操作。

对于单点修改的操作,具体实现如下:

Copy
inline void ins(int &x,int pre,int l,int r,int q,int v)//在某个版本修改某个值 { x=++cnt;ls[x]=ls[pre];rs[x]=rs[pre];val[x]=val[pre];//复制节点 if(l==r){val[x]=v;return ;}//修改当前节点的值 int mid=(l+r)>>1;//计算mid的值 if(q<=mid)ins(ls[x],ls[pre],l,mid,q,v);//根据版本去修改 else ins(rs[x],rs[pre],mid+1,r,q,v); }

其中 x 是当前版本的编号,因为要进行赋值的操作,所以需要加传址符,当然也有不用传址符的,需要你最后返回当前编号的值,在递归的过程中对于每个新开的节点用 ins 函数进行赋值。pre表示的是上一个版本,也就是 x 版本是基于哪一个版本进行修改的, l,r 即为当前节点所代表的区间左右端点,而 q 是要修改的值的位置,v 是要修改成的值。

对于单点的查询操作,我们则通过以下代码实现:

Copy
inline int sum(int x,int l,int r,int q)//区间求值 { if(l==r)return val[x];//如果当前点就是要差查的值就直接返回 int mid=(l+r)>>1;//计算mid的值 if(q<=mid)return sum(ls[x],l,mid,q);//按编号查询 else return sum(rs[x],mid+1,r,q); }

x 是要查询的版本,l,r 是当前点代表的区间,p 是要查询的值的编号(位置)。

这个地方和普通的线段树没有多大区别,所以不细说了。

下面是模板题。

P3919 【模板】可持久化线段树 1(可持久化数组)#

这个题目就是单点修改单点查询,可以配合代码以及注释理解一下。

Copy
#include<bits/stdc++.h> #define N 1000010 using namespace std; int a[N],n,m,q,rt[N*30];//a存放输入的每一个点的值,rt存放每一个版本的根节点 int ls[N*30],rs[N*30],val[N*30],cnt;//存放每一个节点的左右儿子,值 inline int read(){int x=0,f=1;char ch=getchar();while(!isdigit(ch)){f=ch!='-';ch=getchar();}while(isdigit(ch)){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}return f?x:-x;} inline void build(int &x,int l,int r)//建树 { x=++cnt;//存编号 if(l==r){val[x]=a[l];return ;}//如果到了叶子节点就直接赋值退出 int mid=(l+r)>>1;//计算mid的值 build(ls[x],l,mid);//递归建左子树 build(rs[x],mid+1,r);//递归建右子树 } inline void ins(int &x,int pre,int l,int r,int q,int v)//在某个版本修改某个值 { x=++cnt;ls[x]=ls[pre];rs[x]=rs[pre];val[x]=val[pre];//复制节点 if(l==r){val[x]=v;return ;}//修改当前节点的值 int mid=(l+r)>>1;//计算mid的值 if(q<=mid)ins(ls[x],ls[pre],l,mid,q,v);//根据版本去修改 else ins(rs[x],rs[pre],mid+1,r,q,v); } inline int sum(int x,int l,int r,int q)//区间求值 { if(l==r)return val[x];//如果当前点就是要差查的值就直接返回 int mid=(l+r)>>1;//计算mid的值 if(q<=mid)return sum(ls[x],l,mid,q);//按编号查询 else return sum(rs[x],mid+1,r,q); } int main() { n=read();m=read(); for(int i=1;i<=n;i++)a[i]=read(); build(rt[0],1,n);//建树,初始版本号:0 for(int i=1;i<=m;i++)//m次操作,m个新版本 { int pre=read(),op=read(),x=read();//输入版本号,操作,序号 if(op==1){int v=read();ins(rt[i],rt[pre],1,n,x,v);}//操作一,修改值 else {cout<<sum(rt[pre],1,n,x)<<endl;rt[i]=rt[pre];} } return 0;//好习惯 }

当然上面的主席树操作并不常用,因为太明显了,更常用的是下面这种处理区间第 k 大的值的问题。

对于这种问题,我们考虑每一个版本维护什么,只要想到了版本维护的是什么就很好做了。

我们想到要进行排序,因为是第 k 大,如果值域太大,还需要进行离散化。

我们对于从 1~n 的所有数,进行一个前缀和类似的维护思想,第 i 个版本表示前 i 个数的出现情况。

然后我们得到了一个第 i 个版本表示前 i 个数的排名情况。

然后我们利用前缀和的思想,用第 r 棵线段树上的值,减去第 l-1 棵树上的值,得到的就是当前 l 到 r 区间内的所有数的排名情况了,我们在查询的时候,递归到当前点,看看左子树的大小是不是大于 k 的值,如果大于了,说明是在左子树,反之是在右子树,递归到叶子节点返回答案即可。

P3834 【模板】可持久化线段树 2#

这题我们需要考虑主席树

首先我们可以想到,我们先从小到大排序离散化一下,然后我们可以一个一个把数插入到主席树上,一共是 n 个版本,然后第 i 个版本代表的是前 i 个数在数列中的排名情况,然后对于 lr 之间的点我们可以直接用前缀和思想,用 i 版本减去 i1 版本的树上节点就可以了。

Copy
#include<bits/stdc++.h> #define N 1000100 #define endl '\n' using namespace std; int n,m,rt[N],ls[N<<5],rs[N<<5]; int cnt,b[N],a[N],val[N<<5]; inline int read(){int x=0,f=1;char ch=getchar();while(!isdigit(ch)){f=ch!='-';ch=getchar();}while(isdigit(ch)){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}return f?x:-x;} inline void build(int &x,int l,int r) { x=++cnt; if(l==r)return ; int mid=(l+r)>>1; build(ls[x],l,mid); build(rs[x],mid+1,r); } inline void add(int &x,int pre,int l,int r,int p) { x=++cnt;ls[x]=ls[pre];rs[x]=rs[pre];val[x]=val[pre]+1; if(l==r)return ; int mid=(l+r)>>1; if(p<=mid)add(ls[x],ls[pre],l,mid,p); else add(rs[x],rs[pre],mid+1,r,p); } inline int ask(int u,int v,int l,int r,int k) { if(l==r)return b[l]; int mid=(l+r)>>1; int num=val[ls[v]]-val[ls[u]]; if(num>=k)return ask(ls[u],ls[v],l,mid,k); else return ask(rs[u],rs[v],mid+1,r,k-num); } signed main() { n=read();m=read(); for(int i=1;i<=n;i++)b[i]=a[i]=read(); sort(b+1,b+n+1); int len=unique(b+1,b+n+1)-b-1; build(rt[0],1,len); for(int i=1;i<=n;i++) { int x=lower_bound(b+1,b+len+1,a[i])-b; add(rt[i],rt[i-1],1,len,x); } for(int i=1;i<=m;i++) { int l=read(),r=read(),x=read(); cout<<ask(rt[l-1],rt[r],1,len,x)<<endl; } return 0; }

P3939 数颜色#

这道题目乍一看好像不好办,因为他需要支持一个交换的操作。

对于查询的操作,我们是可以直接用跟上面那题一样的前缀和思想来做的,而对于交换的操作,我们可以把交换操作拆成两部分,对于交换 ab 我们可以看成是在当前版本上减去原来的 a 值,然后再加上 b 的值,最后交换一下原数组中两数的值即可。

Copy
#include<bits/stdc++.h> #define N 1000100 #define endl '\n' using namespace std; int n,m,rt[N],val[N<<5],ls[N<<5],rs[N<<5],cnt; int a[N],b[N],vis[N]; inline int read(){int x=0,f=1;char ch=getchar();while(!isdigit(ch)){f=ch!='-';ch=getchar();}while(isdigit(ch)){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}return f?x:-x;} inline void add(int &x,int pre,int l,int r,int p,int v) { x=++cnt;ls[x]=ls[pre],rs[x]=rs[pre],val[x]=val[pre]+v; if(l==r)return ;int mid=(l+r)>>1; if(p<=mid)add(ls[x],ls[pre],l,mid,p,v); else add(rs[x],rs[pre],mid+1,r,p,v); } inline int ask(int u,int v,int l,int r,int p) { if(l==r)return val[v]-val[u];int mid=(l+r)>>1; if(p<=mid)return ask(ls[u],ls[v],l,mid,p); else return ask(rs[u],rs[v],mid+1,r,p); } signed main() { n=read(),m=read(); for(int i=1;i<=n;i++)b[i]=a[i]=read(); sort(b+1,b+n+1); int len=unique(b+1,b+n+1)-b-1; for(int i=1;i<=n;i++) { int o=a[i]; vis[o]=a[i]=lower_bound(b+1,b+len+1,a[i])-b; add(rt[i],rt[i-1],1,len,a[i],1); } for(int i=1;i<=m;i++) { int op=read(); if(op==1) { int l=read(),r=read(),k=read(); if(!vis[k]){cout<<"0"<<endl;continue;} cout<<ask(rt[l-1],rt[r],1,len,vis[k])<<endl; } else { int k=read(); if(a[k]==a[k+1])continue; add(rt[k],rt[k],1,len,a[k],-1); add(rt[k],rt[k],1,len,a[k+1],1); swap(a[k],a[k+1]); } } return 0; }
posted @   北烛青澜  阅读(39)  评论(4编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示
目录