引入一道题
概念
主席树是一种基于线段树的算法
可持续化?
就是主席树很持久,可以支持回退到原来的版本去访问值
例如:查询版本 t 的第k个元素,修改版本 t 的第k个元素
俺只会单点修改的主席树
如何实现
很显然,不能直接对每个版本都去建一棵全新的线段树,因为时间和空间都不允许。
仔细思考修改一个节点线段树发生什么变化???
可以看出,事实上每次修改,只有logn个节点改变了值,对于“第二颗线段树”,我们可以去只增加这logn个节点,并与上一颗线段树共用其余的所有节点
一秒懂图
引入一个名词:动态开点
对于普通的线段树,实际上它是一棵完全二叉树,即每个节点的左右孩子分别为2 *i和2 *i+1,这样的一棵完全二叉树不需要去保存其左右孩子节点的编号。
对于图中的那棵树,除原版本外的其他版本的线段树左右孩子节点均不确定,则需要在递归的过程中,将子节点编号返回给父节点并保存
代码实现
因为要动态开点,其节点要保存其左右子节点的编号,我们开一个结构体。root[i]存储第i个版本的根节点编号,cnt为版本总数
struct node{
int l,r,v;
}tree[N*4];
建树
和原本的建树的区别就是,递归的时候要返回子节点编号给父节点
动态开点:top存储编号用到了多少(避免编号使用重复)
int build(int node,int l,int r){
node=++top;
if(l==r){
tree[node]=a[l];
return node;
}
int mid=(l+r)/2;
tree[node].l=build(tree[node].l,l,mid);
tree[node].r=build(tree[node].r,mid+1,r);
}
int main(){
root[0]=build(1,1,n);
return 0;
}
更新
因为更新从根节点往下走,其一边要与上一棵线段树共用,另一边存储那logn的新节点
int clone(int node){
top++;
tree[top]=tree[node];
return top;
}
int update(int node,int l,int r,int x,int k){
node=clone(node);
if(l==r){
tree[node].v+=k;
return node;
}
int mid=(l+r)/2;
if(x<=mid)tree[node].l=update(tree[node].l,l,mid,x,k);
else tree[node].r=update(tree[node].r,mid+1,r,x,k);
return node;
}
int main(){
root[++cnt]=update(root[t],1,n,x,k);
return 0;
}
查询
查询不修改值,无需增加那logn个节点,只需完整的复制一棵线段树即可,即root[++cnt]=root[t]
int query(int node,int l,int r,int x){
if(l==r)return tree[node];
int mid=(l+r)/2;
if(x<=mid)return query(tree[node].l,l,mid,x);
else return query(tree[node].r,mid+1,r,x);
}
int main(){
cin>>x;
cout<<query(root[t],1,n,x)<<endl;
root[++cnt]=root[t];
return 0;
}
完整代码奉上
#include <bits/stdc++.h>
using namespace std;
const int N=1e7+10;
struct ppp{
int l,r,v;
}tree[N*4];
int top;//总节点数
int cnt=0;//总版本数
int root[N];//每个版本的根
int a[N];
int clone(int node){
top++;
tree[top]=tree[node];
return top;
}
int build(int node,int l,int r){
node=++top;
if(l==r){
tree[node].v=a[l];
return node;
}
int mid=(l+r)/2;
tree[node].l=build(tree[node].l,l,mid);
tree[node].r=build(tree[node].r,mid+1,r);
return node;
}
int update(int node,int l,int r,int x,int k){
node=clone(node);
if(l==r){
tree[node].v=k;
return node;
}
int mid=(l+r)/2;
if(x<=mid)tree[node].l=update(tree[node].l,l,mid,x,k);
else tree[node].r=update(tree[node].r,mid+1,r,x,k);
return node;
}
int query(int node,int l,int r,int x){
if(l==r){
return tree[node].v;
}
int mid=(l+r)/2;
if(x<=mid)return query(tree[node].l,l,mid,x);
else return query(tree[node].r,mid+1,r,x);
}
int main()
{
int n,q,key,local,value;
int t;
cin>>n>>q;
for(int i=1;i<=n;i++)cin>>a[i];
root[0]=build(0,1,n);
for(int i=1;i<=q;i++){
scanf("%d%d",&t,&key);
if(key==1){
scanf("%d%d",&local,&value);
root[++cnt]=update(root[t],1,n,local,value);
}
else {
scanf("%d",&local);
cout<<query(root[t],1,n,local)<<endl;
root[++cnt]=root[t];
}
}
return 0;
}
问题升级
有一个暴力的思路:就是对每个询问区间,都重建一颗线段树并执行一次权值线段树的查找的过程。显然,时间和空间都不允许。
想想别的办法???
我们对整个数组的每一个前缀都建立一棵权值线段树,为啥?我们发现前缀相减对实现查找k的操作不影响。而如何去建立这么多棵权值线段树呢?我们仔细思考发现,前缀i+1和前缀i只有一个点不相同,其他点完全可以共用,我们一下子就想到了主席树,
思路一下子就清晰了!!!
如何实现
一.离散化
因为值域过大,我们需要对数组先进行一次离散化,而又因为答案要输出原来真正的数字,我们还需要开一个数组pos去记录原来的值
int main(){
for(int i=1;i<=n;i++){
cin>>a[i];
b[i]=a[i];
a2[i]=a[i];
}
sort(b+1,b+n+1);
int t=unique(b+1,b+n+1)-b-1;
for(int i=1;i<=n;i++){
a[i]=lower_bound(b+1,b+t+1,a[i])-b;
pos[a[i]]=a2[i];
}
return 0;
}
二.建树
build建立一棵空树即可,因为离散化后值域变成了[1,t],所以build以及update的范围均变为[1,t]
void pushup(int node){
tree[node].v=tree[tree[node].l].v+tree[tree[node].r].v;
}
int build(int node,int l,int r){
node=++top;
if(l==r){
tree[node].v=0;
return node;
}
int mid=(l+r)/2;
tree[node].l=build(tree[node].l,l,mid);
tree[node].r=build(tree[node].r,mid+1,r);
pushup(node);
return node;
}
void solve(int t){
root[0]=build(0,1,t);
}
三.建立前缀线段树(单点修改)
对于每个点,都建立一棵logn的线段树,其他点均与上一棵前缀线段树共用节点,范围同样是值域[1,t]
int clone(int node){
top++;
tree[top]=tree[node];
return top;
}
int update(int node,int l,int r,int x,int k){
node=clone(node);
if(l==r){
tree[node].v+=k;
return node;
}
int mid=(l+r)/2;
if(x<=mid)tree[node].l=update(tree[node].l,l,mid,x,k);
else tree[node].r=update(tree[node].r,mid+1,r,x,k);
pushup(node);
return node;
}
void solve(int t){
for(int i=1;i<=n;i++){
int up=root[cnt];
root[++cnt]=update(up,1,t,a[i],1);
}
}
四.查找区间第k小
利用前缀减,区间[x,y]就是tree[y]-tree[x-1]
int query(int l,int r,int nodex,int nodey,int k){
if(l==r)return l;
int sum=tree[tree[nodey].l].v-tree[tree[nodex].l].v;
//sum为[l,mid]的所有点个数
int mid=(l+r)/2;
if(k<=sum)return query(l,mid,tree[nodex].l,tree[nodey].l,k);
else return query(mid+1,r,tree[nodex].r,tree[nodey].r,k-sum);
}
void solve(int t){
int x,y,k;
for(int i=1;i<=q;i++){
cin>>x>>y>>k;
cout<<pos[query(1,t,root[x-1],root[y],k)]<<endl;
}
}
完整代码奉上
#include<bits/stdc++.h>
using namespace std;
const int N=5e6+10;
struct ppp{
int l,r,v;
}tree[N*4];
int top,cnt,root[N],n,q;
int a[N],b[N],a2[N],pos[N];
void pushup(int node){
tree[node].v=tree[tree[node].l].v+tree[tree[node].r].v;
}
int build(int node,int l,int r){
node=++top;
if(l==r){
tree[node].v=0;
return node;
}
int mid=(l+r)/2;
tree[node].l=build(tree[node].l,l,mid);
tree[node].r=build(tree[node].r,mid+1,r);
pushup(node);
return node;
}
int clone(int node){
top++;
tree[top]=tree[node];
return top;
}
int update(int node,int l,int r,int x,int k){
node=clone(node);
if(l==r){
tree[node].v+=k;
return node;
}
int mid=(l+r)/2;
if(x<=mid)tree[node].l=update(tree[node].l,l,mid,x,k);
else tree[node].r=update(tree[node].r,mid+1,r,x,k);
pushup(node);
return node;
}
int query(int l,int r,int nodex,int nodey,int k){
if(l==r)return l;
int sum=tree[tree[nodey].l].v-tree[tree[nodex].l].v;
int mid=(l+r)/2;
if(k<=sum)return query(l,mid,tree[nodex].l,tree[nodey].l,k);
else return query(mid+1,r,tree[nodex].r,tree[nodey].r,k-sum);
}
void solve(int t){
root[0]=build(0,1,t);
for(int i=1;i<=n;i++){
int up=root[cnt];
root[++cnt]=update(up,1,t,a[i],1);
}
int x,y,k;
for(int i=1;i<=q;i++){
cin>>x>>y>>k;
cout<<pos[query(1,t,root[x-1],root[y],k)]<<endl;
}
}
int main(){
cin>>n>>q;
for(int i=1;i<=n;i++){
cin>>a[i];
b[i]=a[i];
a2[i]=a[i];
}
sort(b+1,b+n+1);
int t=unique(b+1,b+n+1)-b-1;
for(int i=1;i<=n;i++){
a[i]=lower_bound(b+1,b+t+1,a[i])-b;
pos[a[i]]=a2[i];
}
solve(t);
return 0;
}