可持久化权值线段树(主席树)笔记

可持久化权值线段树(主席树)笔记

区别于普通线段树,权值线段树维护的信息不同

  • 普通线段树:节点区间是序列的下标区间,维护区间最值,区间和等信息
  • 权值线段树:节点区间是序列的值域,维护值域内数出现的次数

*图片引自董晓算法


给定一个区间,询问该区间内的第 \(k\) 小值是多少,暴力的方案就是每次都开一颗线段树,由于空间受限我们并不能这样做

一种可行且有效的办法就是建立主席树,保存每次插入时的历史版本,下一次直接从这个版本上衍生节点即可

#define lc(x) tr[x].ch[0]
#define rc(x) tr[x].ch[1]
struct node{
int ch[2];//左右儿子
int s;//记录值域内的元素个数
};
int n,m;
int a[N];
node tr[N*22];//建议开到log^2(n)的大小
int root[N],idx;

首先是建树过程,与线段树类似,递归创建值域 \([1,n]\) 内的各个节点,以及对节点进行编号

build(root[0],1,n);
void build(int &x,int l,int r){
x=++idx;//更新编号
if (l==r) return;//递归出口
int m=l+r>>1;
build(lc(x),l,m);//递归建立左子树
build(rc(x),m+1,r);//递归建立右子树
}

需要对 \(x\) 传入引用,即进行写入操作,更改节点原有值


插入操作,建立主席树,递归创建各个版本的线段树

insert(root[i-1],root[i],1,n,v);
void insert(int x,int &y,int l,int r,int v){//插入v
y=++idx;//对节点进行编号
tr[y]=tr[x];//复制上个版本的信息到当前版本
tr[y].s++;//因为插入了一个数,所以该值域内的元素个数加1
if (l==r) return;
int m=l+r>>1;//二分
if (v<=m) insert(lc(x),lc(y),l,m,v);//依然传入上个版本的儿子与当前版本的引用
else insert(rc(x),rc(y),m+1,r,v);
}

x 传入的是上一个版本的线段树的内容,不引用进行只读操作,y 传入的是当前版本来进行建树,写入操作

同样的 lc(x)lc(y) 相当于两个指针,前者指向上一个版本,后者指向当前版本,进行同步搜索


查询操作,查询区间 \([l,r]\) 内的第 \(k\)

考虑普通情况:查询区间 \([1,r]\) 内的第 \(k\) 小,那么找到插入 \(r\) 时的版本在树上进行搜索即可

那么求 \([l,r]\) 上的第 \(k\) 小,可以采取类似前缀和的处理方式,分别查询 \([1,l-1]\)\([1,r]\) 的信息,做差即可

query(root[l-1],root[r],1,n,k);
int query(int x,int y,int l,int r,int k){
if (l==r) return l;
int m=l+r>>1;
int s=tr[lc(y)].s-tr[lc(x)].s;
if (k<=s) return query(lc(x),lc(y),l,m,k);
else return query(rc(x),rc(y),m+1,r,k-s);//减去左子树值域内增加元素的个数
}

其中 int s=tr[lc(y)].s-tr[lc(x)].s; 计算的是两个版本节点对应左子树值域内元素的增加个数

如果左子树值域内增加的个数小于等于我们需要求的第 \(k\) 小,那么这个元素必然在左子树值域内增加的元素中

如果左子树值域内增加的个数大于我们需要求的第 \(k\) 小,那么这个元素必然在右子树值域内增加的元素中


考虑需要处理的数值值域较大但数量较少时,可以采取离散化的操作,一一映射下标

vector<int> v;
for (int i=1;i<=n;i++){
cin>>a[i];
v.push_back(a[i]);
}
sort(v.begin(),v.end());//从小到大排序
v.erase(unique(v.begin(),v.end()),v.end());//去重
int getid(int x){//得到离散化后的值,一一映射关系
return lower_bound(v.begin(),v.end(),x)-v.begin()+1;
}

主席树查询区间内小于等于 \(k\) 的元素个数

int rankquery(int x,int y,int l,int r,int k){
if (r<=k) return tr[y].s-tr[x].s;//如果k比区间内所有值都要大,那么直接返回区间内元素总数
if (l>k) return 0;//如果k比区间内所有值都要小,那么直接返回区间内元素总数
int m=l+r>>1;//二分
return rankquery(lc(x),lc(y),l,m,k)+rankquery(rc(x),rc(y),m+1,r,k);//递归左右子树搜索
}
posted @   才瓯  阅读(6)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· .NET10 - 预览版1新功能体验(一)
点击右上角即可分享
微信分享提示