首页 > 其他分享 >可持久化线段树学习笔记

可持久化线段树学习笔记

时间:2023-10-26 22:36:06浏览次数:47  
标签:持久 rs int 线段 tr mid 笔记 ls sum

主席树的定义

主席树,也称可持久化线段树,什么是可持久化线段树呢,即为一颗记录了所有更新过程的线段树。能够处理出从第 $i$ 次更新到第 $j$ 次更新的线段树变化。

前置知识

值域线段树

值域线段树的区间存的并不是节点信息,而是在值在某一范围内的数的个数。

如图就是一棵值域线段树。

输入图片说明
1号节点存储的是 大于等于1小于等于4的数字个数

2号节点存储的是 大于等于1小于等于2的数字个数

3号节点存储的是 大于等于3小于等于4的数字个数

4号节点存储的是 等于1的数字个数

5号节点存储的是 等于2的数字个数

6号节点存储的是 等于3的数字个数

7号节点存储的是 等于4的数字个数

查询该区间第 $k$ 大与平衡树相类似,此处不再赘述。

动态开点线段树

原本的线段树我们根据父亲节点为k 子树为k<<1,k<<1|1.

空间复杂度为4N

现在我们直接存储每个节点的左右儿子。可以有效减少空间消耗

动态开点实现的线段树1

code


struct node{
	int ls,rs,sum,tag;
}tr[N];
int n,m,cnt=1;//一定要赋值成1
inline void push_up(int k){
	tr[k].sum=tr[tr[k].ls].sum+tr[tr[k].rs].sum;
}
inline void push_down(int k,int l,int r){
    if(tr[k].tag){
        if(!tr[k].ls)tr[k].ls=++cnt;
        if(!tr[k].rs)tr[k].rs=++cnt;
        tr[tr[k].ls].tag+=tr[k].tag;
       	tr[tr[k].rs].tag+=tr[k].tag;
        int mid=(l+r)>>1;
        tr[tr[k].ls].sum+=(mid-l+1)*tr[k].tag;
        tr[tr[k].rs].sum+=(r-mid)*tr[k].tag;
        tr[k].tag=0;
    }
}
inline void change(int &k,int l,int r,int x,int y,int val){
	if(!k)k=++cnt;
	if(x<=l&&r<=y) {
        tr[k].tag+=val;
		tr[k].sum+=val*(r-l+1);
        return;
    }
    int mid=(l+r)>>1;
    push_down(k,l,r);
    if(x<=mid)change(tr[k].ls,l,mid,x,y,val);
    if(y>mid)change(tr[k].rs,mid+1,r,x,y,val);
    push_up(k);
}
inline int ask(int k,int l,int r,int x,int y){
    if(!k)return 0;
    if(x<=l&&y>=r)return tr[k].sum;
    push_down(k,l,r);
    int mid=(l+r)>>1,res=0;
    if(x<=mid)res+=ask(tr[k].ls,l,mid,x,y);
    if(y>mid)res+=ask(tr[k].rs,mid+1,r,x,y);
    return res;
}
signed main(){
	n=read();m=read();
	int x;
	up(i,1,n){
		x=read();
		int temp=1;
		change(temp,1,n,i,i,x);
	}
	//cout<<tr[1].sum<<endl;
	int op,l,r,t;
	while(m--){
		op=read();
		if(op==1){
			l=read();r=read();t=read();
			int temp=1;
			change(temp,1,n,l,r,t);
		}
		if(op==2){
			l=read();r=read();
			printf("%lld\n",ask(1,1,n,l,r));
		}
	}
    return 0;
}

主席树

由前面的两种知识,如何转化成主席树。

主席树经典问题1:求区间第k大/小。

考虑建 $n$ 棵值域线段树,每棵值域线段树存储区间 $[1,i]$ 的信息,这样一来,要查询 $[l,r]$ 的第 $k$ 大时,只要在查询的过程中,将第$r$ 棵值域线段树的信息减去第 $l−1$ 棵值域线段树的信息即可,这利用了前缀和的思想。

但是建 $k$ 棵值域线段树,不论是时间还是空间,复杂度都是相当劣的。怎样优化呢?

3 5 8 6 7 2 1 4

建立值域线段树。

输入图片说明

我们发现每次加入一个新的元素时更改的部分只会是一条链,而其他的部分则是无用的节点,自然的我们就想到能否让这两棵树共用这部分节点来减少节点的数量和建树的时间。

怎么操作呢?我们先建根结点,递归去看左孩子和右孩子,会发现左孩子的信息和上一棵树的是一样的,所以让他的左孩子直接指向上一棵树的左孩子,体现在代码中就是tr[k].ls=tr[last].ls

继续地递归且按照这种方式操作一直到叶子节点,这样我们就初步完成一颗最简单的主席树了。

当然,可持久化线段树难以支持大部分“区间修改”。

求区间第k大

[l,r]可以看做[1,r]-[1,l-1]

两边双重递归,相减得出来的值就是答案。

code

int n,m;
int rt[N];
struct node{
	int ls,rs,sum;
}tr[N<<5];
int a[N],cnt,b[N];
inline void build(int &k,int l,int r){
	k=++cnt;
	if(l==r){
		tr[k].sum=a[l];
		return;
	}
	int mid=(l+r)>>1;
	build(tr[k].ls,l,mid);
	build(tr[k].rs,mid+1,r);
}
inline void change(int &k,int pre,int l,int r,int x,int y){
	k=++cnt;
	tr[k]=tr[pre];tr[k].sum++;
	if(l==r)return;
	int mid=(l+r)>>1;
	if(x<=mid)change(tr[k].ls,tr[pre].ls,l,mid,x,y);
	if(y>mid)change(tr[k].rs,tr[pre].rs,mid+1,r,x,y);
}
inline int ask(int ll,int rr,int l,int r,int kth){
	int x=tr[tr[rr].ls].sum-tr[tr[ll].ls].sum;
    if(l==r) return b[l];
    int mid=(l+r)>>1;
    if(x>=kth) return ask(tr[ll].ls,tr[rr].ls,l,mid,kth);
    return ask(tr[ll].rs,tr[rr].rs,mid+1,r,kth-x);
}
signed main(){
	n=read();m=read();
	up(i,1,n){
		a[i]=read();
		b[i]=a[i];
	}
	sort(b+1,b+1+n);
	int maxl=unique(b+1,b+1+n)-b-1;
	up(i,1,n){
		int x=lower_bound(b+1,b+1+maxl,a[i])-b;
		change(rt[i],rt[i-1],1,maxl,x,x);
	}
	int op,l,r,pre,x;
    while(m--){
        int l=read(),r=read(),k=read();
        write(ask(rt[l-1],rt[r],1,maxl,k),1);
    }
    return 0;
}

可持久化线段树经典问题

可持久化线段树1

对区间历史进行修改,访问。

code

int n,m;
int rt[N];
struct node{
   int ls,rs,sum,tag;
}tr[N<<5];
int a[N],cnt;
inline void build(int &k,int l,int r){
   k=++cnt;
   if(l==r){
   	tr[k].sum=a[l];
   	return;
   }
   int mid=(l+r)>>1;
   build(tr[k].ls,l,mid);
   build(tr[k].rs,mid+1,r);
}
inline void change(int &k,int pre,int l,int r,int x,int y,int val){
   k=++cnt;
   tr[k].ls=tr[pre].ls;tr[k].rs=tr[pre].rs;
   tr[k].sum=tr[pre].sum;
   if(l==r){
   	tr[k].sum=val;
   	return;
   }
   int mid=(l+r)>>1;
   if(x<=mid)change(tr[k].ls,tr[pre].ls,l,mid,x,y,val);
   if(y>mid)change(tr[k].rs,tr[pre].rs,mid+1,r,x,y,val);
}
inline int ask(int k,int l,int r,int x,int y){
   if(l==r)return tr[k].sum;
   int mid=(l+r)>>1;
   if(mid>=x)return ask(tr[k].ls,l,mid,x,y);
   else return ask(tr[k].rs,mid+1,r,x,y);
}
signed main(){
   n=read();m=read();
   up(i,1,n)a[i]=read();
   build(rt[0],1,n);
   int op,l,r,pre,x;
   up(i,1,m){
   	pre=read();op=read();
   	if(op==1){
   		l=read();x=read();
   		change(rt[i],rt[pre],1,n,l,l,x);
   	}
   	if(op==2){
   		l=read();
   		write(ask(rt[pre],1,n,l,l),1);
   		rt[i]=rt[pre];
   	}
   }
   return 0;
}

询问区间某种颜色数量(可修)

struct node{
	int l,r,sum;
}tr[N<<6];
int cnt,n,m,a[N],rk[N];
inline void push_up(int p){
	tr[p].sum=tr[tr[p].l].sum+tr[tr[p].r].sum;
}
inline void update(int &p,int k,int l,int r,int val){
	if(!p) p=++cnt;
    if(l==r){
        tr[p].sum+=val;
        return ;
    }
    int mid=(l+r)>>1;
    if(k<=mid) update(tr[p].l,k,l,mid,val);
    else update(tr[p].r,k,mid+1,r,val);
	push_up(p);
}

inline int ask(int p,int l,int r,int x,int y){
    if(!p) return 0;
    if(x<=l&&r<=y)return tr[p].sum;
    int mid=(l+r)>>1,ans=0;
    if(x<=mid) ans+=ask(tr[p].l,l,mid,x,y);
    if(mid<y) ans+=ask(tr[p].r,mid+1,r,x,y);
    return ans;
}
signed main(){
	n=read();m=read();
    up(i,1,n){
        a[i]=read();
        update(rk[a[i]],i,1,n,1);
    }
    while(m--){
        int op=read(),l,r,x;
        if(op==1){
            l=read(),r=read(),x=read();
            int ans=ask(rk[x],1,n,l,r);
            printf("%d\n",ans);
        }
        else{
            x=read();
            update(rk[a[x]],x,1,n,-1);
            update(rk[a[x+1]],x+1,1,n,-1);
            update(rk[a[x]],x+1,1,n,1);
            update(rk[a[x+1]],x,1,n,1);
        	int t=a[x];
			a[x]=a[x+1];
			a[x+1]=t;
		}
    }
    return 0;
}

询问静态区间不同数的数量

这种问题一般是用莫队来解决,不过如果强制在线的话,就必须要用主席树或者是树套树,树套树支持动态修改但是比较慢,如果是静态区间可以用主席树。

如何用主席树对这种东西进行维护?

tr[k].sum 表示 $k$ 所代表的区间的答案。
我们记录一个 $last$ 表示该数上一次出现是在那里。

如果这是第一次出现,当前版本答案加一,否则,新建一个版本为减去前一次出现的版本,再把当前版本加一。

int n,m;
int a[N],lst[N],pos[N];
int rt[N];
struct node{
	int ls,rs,sum;
}tr[N<<5];
int cnt;
inline void change(int&k,int pre,int p,int val,int l,int r){
	k=++cnt;
	tr[k]=tr[pre];
	tr[k].sum+=val;
	if(l==r)return;
	int mid=(l+r)>>1;
	if(p<=mid)change(tr[k].ls,tr[pre].ls,p,val,l,mid);
	else change(tr[k].rs,tr[pre].rs,p,val,mid+1,r);
}
inline int ask(int idx,int k,int l,int r) {
	if(l==r)return tr[k].sum;
	int mid=(l+r)>>1;
	if (idx<=mid) return ask(idx,tr[k].ls,l,mid)+tr[tr[k].rs].sum;
	else return ask(idx,tr[k].rs,mid+1,r);
}
signed main(){
    n=read();
	up(i,1,n){
		a[i]=read();
		if(lst[a[i]]==0){
			change(rt[i],rt[i-1],i,1,1,n);
		}
		else{
			int x;
			change(x,rt[i-1],lst[a[i]],-1,1,n);
			change(rt[i],x,i,1,1,n);
		}
		lst[a[i]]=i;
	}
	m=read();
	int l,r;
	while(m--){
		l=read();r=read();
		write(ask(l,rt[r],1,n),1);
	}
	return 0;
}

标签:持久,rs,int,线段,tr,mid,笔记,ls,sum
From: https://www.cnblogs.com/LiQXing/p/17790618.html

相关文章

  • ROS2 foxy 单目相机标定方法(笔记本电脑摄像头)
    环境:Ubuntu20.04、ROS2foxy相机标定使用的是棋盘格的内部顶点,因此"12x9"的棋盘板,其内部顶点参数为"11x8"。安装ImagePipeline安装相机标定所需软件包:sudoaptinstallros-galactic-camera-calibration-parserssudoaptinstallros-galactic-camera-info-managers......
  • python进阶知识体系md笔记14大体系200页,第2章:linux基础命令学习
    本文从14大模块展示了python高级用的应用。分别有Linux命令,多任务编程、网络编程、Http协议和静态Web编程、html+css、JavaScript、jQuery、MySql数据库的各种用法、python的闭包和装饰器、mini-web框架、正则表达式等相关文章的详细讲述。完整版笔记直接地址:请移步这里共14......
  • 《Unix/Linux系统编程》教材学习笔记第四章
    chapter4并行计算早期计算机大多数受到硬件限制,计算机程序通常为串行计算编写的。但是基于分治原则的算法经常表现出高度的并行性,可通过并行或并发执行来提高计算速度。顺序算法与并行算法在描述顺序算法时,常用的方法是用一个begin-end代码块列出算法,如下图左侧所示。begin-en......
  • ORBSLAM3+ROS2foxy 调用笔记本摄像头跑单目相机程序 (Ubuntu20.04)
    环境要求:Ubuntu20.04、ROS2foxy、OpenCV4.4.01.安装ORB_SLAM3首先安装ORB_SLAM3:https://github.com/zang09/ORB-SLAM3-STEREO-FIXED。安装方法参考:https://www.cnblogs.com/xiaoaug/p/17766112.html安装完成并且测试数据集也能够跑通后即可。2.下载ROS2foxy版ORB_......
  • 学习笔记:概率期望
    概率&期望样本空间、随机事件定义一个随机现象中可能发生的不能再细分的结果被称为样本点。所有样本点的集合称为样本空间,通常用\(\Omega\)来表示。一个随机事件是样本空间\(\Omega\)的子集,它由若干样本点构成,用大写字母\(A,B,C,\cdots\)表示。对于一个随机现......
  • usb2.0协议复习--Apple的学习笔记
    一,前言10多年前买过一本圈圈教你usb,然后自己移植了代码到自己焊接的单片机最小系统,当时连原理图都是我自己画的,现在原理图软件已经不知道怎么用了,所以usb协议基本也忘记了。居然配置了usbhost那么简单,这样感觉都没有学习过什么,我还是希望要雁过留痕。所以下载了wiresharkusb抓包......
  • 广义 SAM 学习笔记
    开CF开到了一道广义SAM,决定来学一学。发现网上确实充斥着各种各样的伪广义SAM,也看到了前人反复修改假板子的过程,所以试着来整理一下这堆奇奇怪怪的问题。当然本文的代码也不保证百分百正确,有误请指出(?前置知识后缀自动机(SAM)的构造及应用其实想写在一起的,但因为太长就......
  • 2023/10/25学习笔记·
    Linux基础命令学习2alias——别名语法:alias 自定义命令=“原始命令”(原始命令中有特殊符号的需要打上引号)例如:vim/etc/sysconfig/network-scripts/ifcfg-ens33这条命令是用来更改网卡的aliasmyvim=“vim/etc/sysconfig/network-scripts/ifcfg-ens33”这样......
  • 2023/10/26学习笔记
    Linux基础命令学习3关于文件的命令cat——查看文件语法:cat [选项]...文件...选项:-A:显示隐藏字符-n:显示行号-b:跳过空白行编辑-s:压缩空白行(压缩回车键)合并文件:cat a b  >c——合并ab文件变成c拓展:tac——反向查看文件rev——将每一行的内容反过来查看more/......
  • 多年学习django知识经验总结,从基础到高手,markdown笔记,共计50页,10大模块。 第(2)期
    Django的主要目的是简便、快速的开发数据库驱动的网站。它强调代码复用,多个组件可以很方便的以"插件"形式服务于整个框架,Django有许多功能强大的第三方插件,你甚至可以很方便的开发出自己的工具包。这使得Django具有很强的可扩展性。它还强调快速开发和DRY(DoNotRepeatYourself)原......