【超好懂的数据结构】可持久化数据结构基础/模板

28 篇文章 0 订阅
3 篇文章 0 订阅

title : 可持久化数据结构基础
date : 2021-10-18
tags : ACM,数据结构
author : Linno


可持久化数组

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

对于每一次操作新建一个根节点,然后动态开辟节点。

对于每一次操作1,更改其版本对应位置的值(单点),

对于每一次操作2,将根节点直接赋值过去。

#include<bits/stdc++.h>
#define inf 0x3f3f3f3f
#define MID int mid=l+r>>1
using namespace std;
const int maxn=1e6+7;

int read(){	int x=0,f=1;char ch=getchar();while(ch<'0'||ch>'9'){if(ch=='-') f=f*-1;ch=getchar();}while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}return x*f;}

int n,m,cnt,rt[maxn],a[maxn];
int v,op,loc,val;

struct node{
	int l,r,val;
}tr[maxn*32];

int clone(int p){
	cnt++;
	tr[cnt]=tr[p];
	return cnt;
}

int build(int p,int l,int r){
	p=++cnt;
	if(l==r){
		tr[p].val=a[l];
		return cnt;
	}
	MID;
	tr[p].l=build(tr[p].l,l,mid);
	tr[p].r=build(tr[p].r,mid+1,r);
	return p;
}

int update(int p,int l,int r,int pos,int val){
	p=clone(p);
	if(l==r){
		tr[p].val=val;
	}else{
		MID;
		if(pos<=mid) tr[p].l=update(tr[p].l,l,mid,pos,val);
		else tr[p].r=update(tr[p].r,mid+1,r,pos,val);
	}
	return p;
}

int query(int p,int l,int r,int pos){
	if(l==r) return tr[p].val;
	MID;
	if(pos<=mid) return query(tr[p].l,l,mid,pos);
	else return query(tr[p].r,mid+1,r,pos);
}

signed main(){
	n=read();m=read();
	for(int i=1;i<=n;i++){
		a[i]=read();
	}
	rt[0]=build(0,1,n);
	for(int i=1;i<=m;i++){
		v=read();op=read();loc=read();
		if(op==1){
			val=read();
			rt[i]=update(rt[v],1,n,loc,val);
		}else{
			cout<<query(rt[v],1,n,loc)<<"\n";
			rt[i]=rt[v];
		}
	}
	return 0;
}

可持久化并查集

luoguP3402 可持久化并查集

因为要回退第k个版本(可持久化),开个主席树。叶子节点存储父亲信息和深度。深度用来按秩合并,对同一颗树上的a,b两点合并属于单点修改logn;询问是否在同一集合,只需在当前版本查找。

#include<bits/stdc++.h>
using namespace std;
const int maxn=5e5+7;
const int mod=1e9+7;

int read(){	int x=0,f=1;char ch=getchar();while(ch<'0'||ch>'9'){if(ch=='-') f=f*-1;ch=getchar();}while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}return x*f;}

int n,m,cnt,rt[maxn],op,a,b,u,v,k;

struct node{
	int l,r,fa,dep; //定义主席树的左右节点,当前节点所在集合以及深度(只和合并有关) 
}tr[maxn*32];

inline void build(int &p,int l,int r){
	p=++cnt; //动态开点 
	if(l==r){
		tr[p].fa=l; //只需要初始化父亲 
		return;
	}
	int mid=(l+r)>>1;
	build(tr[p].l,l,mid);
	build(tr[p].r,mid+1,r);
}

inline void merge(int x,int &y,int l,int r,int pos,int fa){ //合并两区间 
	y=++cnt; //动态开点
	tr[y].l=tr[x].l;
	tr[y].r=tr[x].r;
	if(l==r){  //属于单点修改 
		tr[y].fa=fa;
		tr[y].dep=tr[x].dep;
		return;
	}
	int mid=l+r>>1;
	if(pos<=mid) merge(tr[x].l,tr[y].l,l,mid,pos,fa);
	else merge(tr[x].r,tr[y].r,mid+1,r,pos,fa);
}

inline void update(int p,int l,int r,int pos){ 
	if(l==r){ //单点修改秩 
		tr[p].dep++;
		return;
	}
	int mid=l+r>>1;
	if(pos<=mid) update(tr[p].l,l,mid,pos);
	else update(tr[p].r,mid+1,r,pos);
}

inline int query(int p,int l,int r,int pos){ //询问pos所在的树上节点 
	if(l==r) return p;
	int mid=l+r>>1;
	if(pos<=mid) return query(tr[p].l,l,mid,pos);
	else return query(tr[p].r,mid+1,r,pos); 
}

inline int find(int p,int pos){
	int now=query(p,1,n,pos);
	if(tr[now].fa==pos) return now; //向上查找父亲
	return find(p,tr[now].fa);
}

signed main(){
	n=read();m=read();
	build(rt[0],1,n);
	for(int i=1;i<=m;i++){
		op=read();
		if(op==1){
			a=read();b=read(); 
			rt[i]=rt[i-1]; //换根 
			u=find(rt[i],a),v=find(rt[i],b);
			if(tr[u].fa!=tr[v].fa){ //按秩合并 
				if(tr[u].dep>tr[v].dep) swap(u,v);
				merge(rt[i-1],rt[i],1,n,tr[u].fa,tr[v].fa);
				if(tr[u].dep==tr[v].dep) update(rt[i],1,n,tr[v].fa);
			}
		}else if(op==2){
			k=read();
			rt[i]=rt[k];
		}else{
			a=read();b=read();
			rt[i]=rt[i-1];
			if(tr[find(rt[i],a)].fa==tr[find(rt[i],b)].fa) puts("1");
			else puts("0");
		}
	}
	return 0;
}

可持久化Trie

对于一棵树上的最大异或和,可以用0/1Trie来解决,但是如果每次询问都是给定区间的话,不能对每个区间都建Trie,这时候就可以用到可持久化Trie了。我们很容易知道,异或是满足可见性的,所以可以用主席树的思想来维护前缀和,两棵树做差即可获得区间的最大异或值。

luoguP4735 最大异或和

给定一个非负整数序列 { a } \{a\} {a},初始长度 n n n

m m m 个操作,有以下两种操作类型:

  1. A x:添加操作,表示在序列末尾添加一个数 x x x,序列的长度 n + 1 n+1 n+1
  2. Q l r x:询问操作,你需要找到一个位置 p p p,满足 l ≤ p ≤ r l \le p \le r lpr,使得:$ a[p] \oplus a[p+1] \oplus … \oplus a[N] \oplus x$ 最大,输出最大是多少。
#include<bits/stdc++.h>
using namespace std;
const int N=6e5+10,M=(N<<5);

int n,m,s[N];
int tr[M][2],max_id[M]; //max_id表示当前新加的节点在前缀和数组s的位置 
int rt[N],idx=0;

inline void insert(int p,int lst,int k){ //Trie插入操作 
	max_id[p]=k;
	for(int i=25;i>=0;--i){
		int v=s[k]>>i&1;
		if(lst) tr[p][v^1]=tr[lst][v^1];
		tr[p][v]=++idx;
		max_id[tr[p][v]]=k;
		p=tr[p][v];lst=tr[lst][v];
	}
}

int query(int l,int r,int C){  //查询[l,r]区间中,与C异或值最大的数 
	int p=rt[r];
	for(int i=25;i>=0;--i){ // C是s[n]^x, 从高位到低位逐位检索二进制每一位上能跟C异或结果最大的数
		int v=C>>i&1;
		if(max_id[tr[p][v^1]]>=l) p=tr[p][v^1];
		else p=tr[p][v];
	}
	return C^s[max_id[p]];
}

int main(){
	scanf("%d%d",&n,&m); // 前缀和,初始化第0个版本
	s[0]=0;
	max_id[0]=-1;
	rt[0]=++idx;
	insert(rt[0],0,0);//一开始先插入一个0
	for(int i=1;i<=n;++i){
		int x;
		scanf("%d",&x);
		s[i]=s[i-1]^x;
		rt[i]=++idx;
		insert(rt[i],rt[i-1],i); 
	}
	char op[2];
	int l,r,x;
	for(int i=1;i<=m;++i){
		scanf("%s",op);
		if(op[0]=='A'){
			scanf("%d",&x);
			++n;
			s[n]=s[n-1]^x;
			rt[n]=++idx;
			insert(rt[n],rt[n-1],n);
		}else{
			scanf("%d%d%d",&l,&r,&x);
			printf("%d\n", query(l-1,r-1,s[n]^x));
		}
	}
	return 0;
}

可持久化线段树

在每一个位置维护一个线段树,离散化后节点可以表示值的范围。然后利用前缀和的思想去将两个版本的线段树做差即可得到[l,r]序列的权值线段树。

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

经典的静态区间第k小问题

#include<bits/stdc++.h>
using namespace std;
const int N=2e5+7;
const int mod=1e9+7;
int read(){	int x=0,f=1;char ch=getchar();while(ch<'0'||ch>'9'){if(ch=='-') f=f*-1;ch=getchar();}while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}return x*f;}

int n,m,cnt=0,rt[N],a[N]; //a[i]为原始序列,rt[i]为第i版本主席树 

struct node{
	int l,r,sum;  //树的左右端点和元素个数 
}tr[N<<5];

vector<int>vt;  //辅助容器 

int getid(int x){ //返回每个数的rank 
	return lower_bound(vt.begin(),vt.end(),x)-vt.begin()+1; 
}

void update(int &x,int y,int l,int r,int pos){
	tr[++cnt]=tr[y];
	tr[cnt].sum++;  //区域元素+1 
	x=cnt; //将原来的树地址指向当前树 
	if(l==r) return; //叶子结点返回 
	int mid=((l+r)>>1);
	if(pos<=mid) update(tr[x].l,tr[y].l,l,mid,pos); //左子树插入 
	else update(tr[x].r,tr[y].r,mid+1,r,pos); //右子树插入 
}

int query(int x,int y,int l,int r,int k){ //查询[l,r]区间第k大 
	if(l==r) return l;
	int mid=((l+r)>>1);
	int sum=tr[tr[y].l].sum-tr[tr[x].l].sum; //左子树相减,其差值为两版本左边相差多少个数
	if(k<=sum) return query(tr[x].l,tr[y].l,l,mid,k); //结果大于等于k,询问左子树 
	else return query(tr[x].r,tr[y].r,mid+1,r,k-sum); //否则找右子树第k-sum小的数 
}

signed main(){
	n=read();m=read();
	for(int i=1;i<=n;i++){
		a[i]=read();
		vt.push_back(a[i]); //辅助容器用于离散化 
	}
	sort(vt.begin(),vt.end()); //排序 
	vt.erase(unique(vt.begin(),vt.end()),vt.end()); //去重 
	for(int i=1;i<=n;i++){ //每次插入都从根节点开始建一棵新树 
		update(rt[i],rt[i-1],1,n,getid(a[i]));
	}
	for(int i=1,x,y,k;i<=m;i++){ //第y和x-1棵树做差,就能查出[x,y]区间第k小 
		x=read();y=read();k=read();
		printf("%d\n",vt[query(rt[x-1],rt[y],1,n,k)-1]);
	}
	return 0;
}

可持久化平衡树

平衡树的可持久化一般可以用FHQ Treap实现。

无旋Treap可通过MergeSplit操作复制路径上的结点(一般在Split操作中复制,确保不会影响以前的版本)就可以完成可持久化。

旋转Treap在复制路径上经过的结点同时,还需复制受选择影响的结点(每次旋转只影响两个结点),不过不会影响时间复杂度。

这种方法称之为path coping

P5055 【模板】可持久化文艺平衡树

题目大意:支持①单点插入;②单点删除;③翻转区间;④区间求和操作并且强制在线

#include<bits/stdc++.h>
#define inf 0x3f3f3f3f
#define ll long long
#define int long long
using namespace std;
const int N=2e5+3;
const int mod=1e9+7;

int read(){	int x=0,f=1;char ch=getchar();while(ch<'0'||ch>'9'){if(ch=='-') f=f*-1;ch=getchar();}while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}return x*f;}

struct FHQ{
	int ls,rs,sz,pri;
	ll val,sum; //左儿子,右儿子,树大小,权值、区间和 
	bool lzy; //两个标记,翻转区间和区间赋值
}T[(N<<7)];

int idx,rt[N]; 

inline int new_node(ll v=0){  //新建结点 
	static int tot(0);
	//T[++tot].ls=T[tot].rs=T[tot].lzy=0; 
	T[++tot].sz=1;T[tot].pri=rand();
	T[tot].val=T[tot].sum=v; 
	return tot; //返回节点编号 
}

inline int copy_node(int p){ 
	int res=new_node();
	T[res]=T[p];
	return res;
}

inline void pushup(int p){ //结点更新信息 
	T[p].sz=T[T[p].ls].sz+T[T[p].rs].sz+1;  //子树大小 
	T[p].sum=T[T[p].ls].sum+T[T[p].rs].sum+T[p].val; //区间和 
}

inline void pushdown(int p){ //下传标记 
	if(!T[p].lzy) return;
	if(T[p].ls) T[p].ls=copy_node(T[p].ls);
	if(T[p].rs) T[p].rs=copy_node(T[p].rs);
	swap(T[p].ls,T[p].rs);
	if(T[p].ls) T[T[p].ls].lzy^=1;
	if(T[p].rs) T[T[p].rs].lzy^=1;
	T[p].lzy=0;
}

inline void split(int p,int k,int &x,int &y){ //分离操作 
	if(!p){x=y=0;return;} 
	pushdown(p); 
	if(T[T[p].ls].sz+1<=k){ 
		x=copy_node(p); 
		split(T[x].rs,k-T[T[p].ls].sz-1,T[x].rs,y); 
		pushup(x); 
	}else{ 
		y=copy_node(p); 
		split(T[y].ls,k,x,T[y].ls); 
		pushup(y); 
	} 
} 

inline int merge(int x,int y){ //合并操作 
	if(!x||!y) return x|y;
	pushdown(x),pushdown(y); 
	if(T[x].pri<T[y].pri){
		T[x].rs=merge(T[x].rs,y);pushup(x);return x;
	}else{
		T[y].ls=merge(x,T[y].ls);pushup(y);return y;
	} 
}

signed main(){
	srand(time(0));
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	int cnt=0;
	int m,v,op,pos,a,b,x,y,z;
	ll lastans=0,val;
	cin>>m;
	for(int i=1;i<=m;i++){ 
		cin>>v>>op;
		if(op==1){ //区间插入操作
			cin>>a>>b;
			a^=lastans,b^=lastans;
			split(rt[v],a,x,y); //以pos位置拆开再进行三棵树的合并 
			rt[++cnt]=merge(merge(x,new_node(b)),y);
		}else if(op==2){ //删除操作 
			cin>>a;
			a^=lastans;
			split(rt[v],a,x,z); //以pos-1位置拆开
			split(x,a-1,x,y);
			rt[++cnt]=merge(x,z); //进行合并 
		}else if(op==3){ //翻转区间 
			cin>>a>>b;
			a^=lastans,b^=lastans;
			split(rt[v],b,x,z);
			split(x,a-1,x,y);
			T[y].lzy^=1;
			rt[++cnt]=merge(merge(x,y),z);
		}else if(op==4){ //区间求和 
			cin>>a>>b;
			a^=lastans,b^=lastans;
			split(rt[v],b,x,z);
			split(x,a-1,x,y);
			printf("%lld\n",lastans=T[y].sum);
			rt[++cnt]=merge(merge(x,y),z);
		}
	}
	return 0;
}

参考资料

OI-Wiki

https://blog.csdn.net/qq_52678569/article/details/124210596

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
哈夫曼树是一种特殊的二叉树结构,用于编码和解码数据。在哈夫曼树中,每个叶子节点都代表一个字符或符号,并且具有一个与之关联的权值,代表该字符或符号出现的频率或概率。根据哈夫曼树的概念,我们可以通过给定的叶子节点的权值来构建哈夫曼树。 对于给定的叶子节点的权值,构建哈夫曼树的步骤如下: 1. 首先,根据叶子节点的权值从小到大进行排序。 2. 选取权值最小的两个叶子节点,并将它们作为两个子节点创建一个新的父节点。新父节点的权值等于这两个子节点的权值之和。 3. 将这个新的父节点插入到叶子节点中,同时删除原来的两个子节点。 4. 重复步骤2和步骤3,直到只剩下一个节点,即根节点,这个节点就是哈夫曼树的根节点。 根据题目提供的例子,我们可以看到一种不是建树的方法,只使用数组来模拟哈夫曼树的构造过程。这种方法是通过数组来存储节点的信息,并通过一些特定的计算方式来模拟构建哈夫曼树的过程。 根据题目的描述,我们需要根据叶子节点的个数和权值来生成哈夫曼树,并计算所有节点的值与权值的乘积之和。这个问题可以通过构建哈夫曼树的步骤来解决。首先,我们需要将叶子节点根据权值进行排序。然后,按照步骤2和步骤3构建哈夫曼树,直到只剩下一个节点。最后,计算所有节点的值与权值的乘积之和。 综上所述,数据结构哈夫曼树的例题是通过给定叶子节点的权值来构建哈夫曼树,并计算所有节点的值与权值的乘积之和。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [超好理解的哈夫曼树(最优二叉树)与例题](https://blog.csdn.net/weixin_45720782/article/details/109316157)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

RWLinno

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值