主席树(可持续化线段树)

引入一道题

P3919主席树模板题

概念

主席树是一种基于线段树的算法
可持续化?
就是主席树很持久,可以支持回退到原来的版本去访问值
例如:查询版本 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;
}

问题升级

P3834静态查找区间第k小

如何用权值线段树求整个区间的第k小

有一个暴力的思路:就是对每个询问区间,都重建一颗线段树并执行一次权值线段树的查找的过程。显然,时间和空间都不允许。

想想别的办法???
我们对整个数组的每一个前缀都建立一棵权值线段树,为啥?我们发现前缀相减对实现查找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;
}

猜你喜欢

转载自blog.csdn.net/weixin_43602607/article/details/109515163