[学习笔记] 可持久化线段树&主席树
众所周知,线段树是一个非常好用也好写的数据结构,
因此,我们今天的前置技能:线段树.
然而,可持久化到底是什么东西?
别急,我们一步一步来...
step 1
首先,一道简化的模型:
给定一个长度为\(n\)的序列,\(m\)个操作,支持两种操作:
- 修改某个点\(i\)的权值
- 查询历史上某个版本\(u\)中点\(i\)的权值
同时,每个操作都会生成一个新的版本(也就是说修改是改的一个新的版本,而查询是直接\(copy\)上一个版本.
那么,暴力的做法来了:
直接维护\(m\)棵线段树,先\(copy\)一遍,再直接修改/查询.
然而时间空间都得炸啊啊啊
别急,让我们仔细想想...
首先,我们考虑一下每次修改会发生什么(看图):
(其中修改的是6号节点,红色是经过的路径),
我们可以发现,每次修改都只改了一条链.
也就是说,对于上一个版本,就只有一条链不一样(查询就是一模一样了).
因此,对于上一个版本中一样的其他的链,我们就可以直接沿用.
比如说,上一个版本长这样:
而沿用后的图就长这样选点时的随意导致了图片的丑陋:
那么,我们就只需要在更新版本时,新建一个根节点\(rt[i]\),
并且只需要新建修改的那条链,其他的沿用上一个版本的就行了.
代码也很简单:
void update(int &k/*动态加的点(当前节点)*/,int p/*沿用的点,即上个版本中这个位置的节点编号*/,int l,int r,int pla/*修改的元素位置*/,int x/*修改的权值*/){
k=++tot;t[k]=t[p];//先copy一波
if(l==r){t[k].val=x;return ;}
int mid=(l+r)>>1;//这里就和线段树一样了
if(pla<=mid) update(t[k].l,t[p].l,l,mid,pla,x);
else update(t[k].r,t[p].r,mid+1,r,pla,x);
}
顺便把建树和询问也贴上来吧(其实和线段树一样):
void build(int &x,int l,int r){
x=++tot;
if(l==r){t[x].val=a[l];return ;}
int mid=(l+r)>>1;
build(t[x].l,l,mid);build(t[x].r,mid+1,r);
}
int query(int p,int l,int r,int x){
if(l==r) return t[p].val;
int mid=(l+r)>>1;
if(x<=mid) return query(t[p].l,l,mid,x);
else return query(t[p].r,mid+1,r,x);
}
到这里,一个简单的模板就结束啦.
例题:[模板]可持久化数组
这题就和模板一模一样,
在叶子节点记录权值,每次单点修改即可.
注意一下,询问是复制的询问的那个版本(不是上一个)因为这个调了好久qwq
上完整代码吧其实都在上面了~:
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
inline int read(){
int sum=0,f=1;char c=getchar();
while(c>'9'||c<'0'){if(c=='-') f=-1;c=getchar();}
while(c<='9'&&c>='0'){sum=sum*10+c-'0';c=getchar();}
return sum*f;
}
const int MX=1000005;
struct tree{int l,r,val;}t[MX*30];
int n,m,a[MX];
int rt[MX],tot=0;
void build(int &x,int l,int r){
x=++tot;
if(l==r){t[x].val=a[l];return ;}
int mid=(l+r)>>1;
build(t[x].l,l,mid);build(t[x].r,mid+1,r);
}
void update(int &k,int p,int l,int r,int pla,int x){
k=++tot;t[k]=t[p];
if(l==r){t[k].val=x;return ;}
int mid=(l+r)>>1;
if(pla<=mid) update(t[k].l,t[p].l,l,mid,pla,x);
else update(t[k].r,t[p].r,mid+1,r,pla,x);
}
int query(int p,int l,int r,int x){
if(l==r) return t[p].val;
int mid=(l+r)>>1;
if(x<=mid) return query(t[p].l,l,mid,x);
else return query(t[p].r,mid+1,r,x);
}
int main(){
n=read();m=read();
for(int i=1;i<=n;i++) a[i]=read();
build(rt[0],1,n);
for(int i=1;i<=m;i++){
int tt=read(),opt=read(),pla=read();
if(opt==1){int x=read();update(rt[i],rt[tt],1,n,pla,x);}
else if(opt==2){printf("%d\n",query(rt[tt],1,n,pla));rt[i]=rt[tt];}
}
return 0;
}
step 2
接下来,就是我们喜闻乐见的主席树了(话说有哪位\(dalao\)能告诉我为什么叫这名字?).
我们以一道模板题开始吧:[模板]可持久化线段树 1(主席树)
给出一个长度为\(n\)的序列,\(m\)个询问,每次询问区间\([l,r]\)中第\(k\)小的数.
这题暴力很好写吧(然而我们并不满足于暴力).
我们还是一步步来,
首先,先考虑下如果是区间\([1,n]\)的第\(k\)小该怎么做:
这时候,我们可以考虑到权值线段树.
将原序列离散化,再建一棵线段树,
但这个线段树维护的区间并不是序列的区间,
而是权值的区间.
比如说区间\([l,r]\),其实指的是离散化后的权值\(l\)~\(r\),
也就是第\(l\)大到第\(r\)大.
还是举个栗子吧:
假设我们现在有一个序列\(a\):1,3,3,5,5,5,8,8,8,8.
那么离散化以后就是:1,2,2,3,3,3,4,4,4,4.
然后我们再建一棵权值线段树:
其中黑色的代表节点编号,红色的代表权值区间,
而蓝色的是接下来我们要讲的每个节点维护的一个值:\(cnt\).
这个\(cnt\)表示的是当前节点的权值区间内有几个节点.
听不懂?没关系我们接着看:
当我们讲第一个元素(在离散化后就是\(1\))插入以后,树就变成了这样:
所有权值区间包括\(1\)的\(cnt\)都加了\(1\).
而当我们将所有数都插进去后,树就成了这样:
于是,我们就可以清楚地看到,在序列中,权值在区间\([l,r]\)的数有多少个.
插入的代码如下:
int update(int p/*节点编号*/,int l,int r/*l,r为权值区间*/,int k/*插入的权值*/){
int x=++tot;t[x]=t[p];t[x].cnt++;
if(l==r) return x;
int mid=(l+r)>>1;
if(k<=mid) t[x].l=build(t[p].l,l,mid,k);
else t[x].r=build(t[p].r,mid+1,r,k);
return x;
}
然后我们在求第\(k\)小时,先与左子树的\(cnt\)比较,
若\(k<=cnt\),那答案就在左子树的权值区间里,
否则,将\(k\)减去\(cnt\),再在右子树里找,
一直到最底层就行了.
查询代码如下:
int query(int p/*当前节点*/,int l,int r,int k/*第k小*/){
if(l==r) return l;
int mid=(l+r)>>1;
if(k<=t[t[p].l].cnt) return query(t[p].l,l,mid,k);
else return query(t[p].r,mid+1,r,k-t[t[p].l].cnt);
}
那么接下来,我们来考虑区间\([l,r]\)的第\(k\)小:
仔细想想,其实我们可以用前缀和的思想,
一个权值为\(x\)的数在\(1\)~\(l-1\)中出现了\(a\)次,
在\(1\)~\(r\)中出现了\(b\)次,
那么它在区间\([l,r]\)中就出现了\(a-b\)次.
因此,我们可以对每个区间\([1,i],i\in [1,n]\)建一颗权值线段树,
在查询时,用前缀和的思想计算\(cnt\),再查找就行啦.
然而到这里就结束了吗?
我们注意到,对于区间\([1,i]\)的权值线段树,
与区间\([1,i-1]\)比起来仅仅是多插入了一个\(i\)的权值而已.
想到了什么? 可持久化线段树!
没错,我们可以像可持久化线段树一样,
沿用相同的节点,只新建需要更新的一条链就行了.
贴上新的询问代码:
int query(int L/*区间[1,l-1]的节点*/,int R/*区间[1,r]的节点*/,int l,int r,int k/*第k小*/){
if(l==r) return l;
int mid=(l+r)>>1,sum=t[t[R].l].cnt-t[t[L].l].cnt;//前缀和
if(sum>=k) return ask(t[L].l,t[R].l,l,mid,k);
else return ask(t[L].r,t[R].r,mid+1,r,k-sum);
}
到这里,主席树就讲完啦.
上完整代码吧:
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
inline int read(){
int sum=0,f=1;char c=getchar();
while(c<'0'||c>'9'){if(c=='-') f=-1;c=getchar();}
while(c>='0'&&c<='9'){sum=sum*10+c-'0';c=getchar();}
return sum*f;
}
struct tree{int cnt,l,r;}t[5000001];
int n,m,a[1000001],c[1000001];
int rt[500001],tot=0;
int build(int p,int l,int r,int k){
int x=++tot;
t[x]=t[p];t[x].cnt++;
if(l==r) return x;
int mid=(l+r)>>1;
if(k<=mid) t[x].l=build(t[p].l,l,mid,k);
else t[x].r=build(t[p].r,mid+1,r,k);
return x;
}
int ask(int L,int R,int l,int r,int k){
if(l==r) return l;
int mid=(l+r)>>1,sum=t[t[R].l].cnt-t[t[L].l].cnt;
if(sum>=k) return ask(t[L].l,t[R].l,l,mid,k);
else return ask(t[L].r,t[R].r,mid+1,r,k-sum);
}
int main(){
n=read();m=read();
for(int i=1;i<=n;i++) a[i]=read();
memcpy(c,a,sizeof(c));sort(c+1,c+n+1);
int T=unique(c+1,c+n+1)-c-1;
for(int i=1;i<=n;i++) a[i]=lower_bound(c+1,c+T+1,a[i])-c;
for(int i=1;i<=n;i++) rt[i]=build(rt[i-1],1,T,a[i]);
for(int i=1;i<=m;i++){
int l=read(),r=read(),k=read();
printf("%d\n",c[ask(rt[l-1],rt[r],1,T,k)]);
}
return 0;
}
以后可能还会更新(埋个坑在这)...
step 3
还真的更新了...
之前,我们讲了可持久化线段树的单点修改对吧.
然而,区间修改去哪了?
但是我们仔细想想,
对于每个版本的线段树,
它们是共用了一些节点,
所以在\(pushdown\) \(tag\)的时候,就会出锅...(自己\(yy\)一下就清楚了)
因此,我们有了一种新的操作——标记永久化.
将一个点的\(tag\)一直存下来,在询问的时候直接加上去.
而在修改的时候,只要被区间覆盖到,就要新建节点.
并且,还要一边切割需要修改的区间(这个等下看代码吧),
一直到完全重合时再返回.
来上修改和查询的代码吧:
int update(int p,int l,int r,int d){
int x=++tot;t[x]=t[p];
t[x].sum+=(r-l+1)*d;
if(t[x].l==l&&t[x].r==r){//完全重合时返回
t[x].tag+=d;return x;
}
int mid=(t[x].l+t[x].r)>>1;
if(r<=mid) t[x].ls=update(t[p].ls,l,r,d);//把要修改的区间切一下
else if(l>mid) t[x].rs=update(t[p].rs,l,r,d);
else t[x].ls=update(t[p].ls,l,mid,d),t[x].rs=update(t[p].rs,mid+1,r,d);
return x;
}
ll ask(int p,int ad/*一路加上的tag*/,int l,int r){
if(t[p].l==l&&t[p].r==r){
return (r-l+1)*ad/*别忘了tag*/+t[p].sum;
}
int mid=(t[p].l+t[p].r)>>1;
if(r<=mid) return ask(t[p].ls,ad+t[p].tag,l,r);
else if(l>mid) return ask(t[p].rs,ad+t[p].tag,l,r);
else return ask(t[p].ls,ad+t[p].tag,l,mid)+ask(t[p].rs,ad+t[p].tag,mid+1,r);
}
接下来,让我们来看道例题吧:洛谷SP11470 TTM - To the moon
这题就是标记永久化的板子了.
看代码吧:
#include <iostream>
#include <cstdio>
#include <cstring>
#define ll long long
using namespace std;
inline int read(){
int sum=0,f=1;char c=getchar();
while(c<'0'||c>'9'){if(c=='-') f=-1;c=getchar();}
while(c>='0'&&c<='9'){sum=sum*10+c-'0';c=getchar();}
return sum*f;
}
struct tree{int l,r,ls,rs;ll sum,tag;}t[2000021];
int n,m,a[500001];
int tt=0,tot=0,rt[500001];
inline void pushup(int p){
t[p].sum=t[t[p].ls].sum+t[t[p].rs].sum;
}
inline void build(int &p,int l,int r){
p=++tot;t[p].l=l;t[p].r=r;
if(l==r){t[p].sum=a[l];return ;}
int mid=(l+r)>>1;
build(t[p].ls,l,mid);build(t[p].rs,mid+1,r);
pushup(p);
}
int update(int p,int l,int r,int d){
int x=++tot;t[x]=t[p];
t[x].sum+=(r-l+1)*d;
if(t[x].l==l&&t[x].r==r){
t[x].tag+=d;return x;
}
int mid=(t[x].l+t[x].r)>>1;
if(r<=mid) t[x].ls=update(t[p].ls,l,r,d);
else if(l>mid) t[x].rs=update(t[p].rs,l,r,d);
else t[x].ls=update(t[p].ls,l,mid,d),t[x].rs=update(t[p].rs,mid+1,r,d);
return x;
}
ll ask(int p,int ad,int l,int r){
if(t[p].l==l&&t[p].r==r){
return (r-l+1)*ad+t[p].sum;
}
int mid=(t[p].l+t[p].r)>>1;
if(r<=mid) return ask(t[p].ls,ad+t[p].tag,l,r);
else if(l>mid) return ask(t[p].rs,ad+t[p].tag,l,r);
else return ask(t[p].ls,ad+t[p].tag,l,mid)+ask(t[p].rs,ad+t[p].tag,mid+1,r);
}
inline void change(){
int l=read(),r=read(),d=read();
rt[tt+1]=update(rt[tt],l,r,d);tt++;
}
inline void query(int x){
int l=read(),r=read(),opt=(x? read():tt);
printf("%lld\n",ask(rt[opt],0,l,r));
}
inline void back(int x){
tt=x;tot=rt[x+1]-1;
}
int main(){
n=read();m=read();
for(int i=1;i<=n;i++) a[i]=read();
build(rt[0],1,n);
for(int i=1;i<=m;i++){
char opt[5];cin>>opt;
if(opt[0]=='C') change();
else if(opt[0]=='Q') query(0);
else if(opt[0]=='H'){query(1);}
else if(opt[0]=='B'){int x=read();back(x);}
}
return 0;
}
[学习笔记] 可持久化线段树&主席树的更多相关文章
- [学习笔记]可持久化数据结构——数组、并查集、平衡树、Trie树
可持久化:支持查询历史版本和在历史版本上修改 可持久化数组 主席树做即可. [模板]可持久化数组(可持久化线段树/平衡树) 可持久化并查集 可持久化并查集 主席树做即可. 要按秩合并.(路径压缩每次建 ...
- 线段树简单入门 (含普通线段树, zkw线段树, 主席树)
线段树简单入门 递归版线段树 线段树的定义 线段树, 顾名思义, 就是每个节点表示一个区间. 线段树通常维护一些区间的值, 例如区间和. 比如, 上图 \([2, 5]\) 区间的和, 为以下区间的和 ...
- 学习笔记--函数式线段树(主席树)(动态维护第K极值(树状数组套主席树))
函数式线段树..资瓷 区间第K极值查询 似乎不过似乎划分树的效率更优于它,但是如果主席树套树状数组后,可以处理动态的第K极值.即资瓷插入删除,划分树则不同- 那么原理也比较易懂: 建造一棵线段树(权值 ...
- 权值线段树&&可持久化线段树&&主席树
权值线段树 顾名思义,就是以权值为下标建立的线段树. 现在让我们来考虑考虑上面那句话的产生的三个小问题: 1. 如果说权值作为下标了,那这颗线段树里存什么呢? ----- 这颗线段树中, 记录每个值出 ...
- 【数据结构模版】可持久化线段树 && 主席树
浙江集训Day4,从早8:00懵B到晚21:00,只搞懂了可持久化线段树以及主席树的板子.今天只能记个大概,以后详细完善讲解. 可持久化线段树指的是一种基于线段树的可回溯历史状态的数据结构.我们想要保 ...
- 洛谷P3834 可持久化线段树(主席树)模板
题目:https://www.luogu.org/problemnew/show/P3834 无法忍受了,我要写主席树! 解决区间第 k 大查询问题,可以用主席树,像前缀和一样建立 n 棵前缀区间的权 ...
- bzoj 4408: [Fjoi 2016]神秘数 数学 可持久化线段树 主席树
https://www.lydsy.com/JudgeOnline/problem.php?id=4299 一个可重复数字集合S的神秘数定义为最小的不能被S的子集的和表示的正整数.例如S={1,1,1 ...
- 牛客网 暑期ACM多校训练营(第一场)J.Different Integers-区间两侧不同数字的个数-离线树状数组 or 可持久化线段树(主席树)
J.Different Integers 题意就是给你l,r,问你在区间两侧的[1,l]和[r,n]中,不同数的个数. 两种思路: 1.将数组长度扩大两倍,for(int i=n+1;i<=2* ...
- [POJ2104] K – th Number (可持久化线段树 主席树)
题目背景 这是个非常经典的主席树入门题--静态区间第K小 数据已经过加强,请使用主席树.同时请注意常数优化 题目描述 如题,给定N个正整数构成的序列,将对于指定的闭区间查询其区间内的第K小值. 输入输 ...
随机推荐
- Linux_目录基本操作_常用命令【详解】
Linux_常用命令 Linux文件系统的目录树结构:[Linux世界里一切皆文件]:说白了,就是文件和文件夹(目录)之间的操作. 普通用户kkb所有文件及文件夹,其实都位于root用户的 /home ...
- django 聚合统计查询
from django.shortcuts import renderfrom django.http import HttpResponsefrom django.db.models import ...
- Linux命令-文件管理篇-cat
1.cat 说明 cat 是一个文本文件查看和连接工具.查看一个文件的内容,用cat比较简单,就是cat 后面直接接文件名. 2.使用权限 所有使用者 <!-- more --> 3.ca ...
- docker-配置网桥-自定义网络
容器网络访问原理 桥接宿主机网络 临时生效: # 网桥名称 br_name=br0 # 添加网桥 brctl addbr $br_name # 给网桥设置IP ip addr add 192.168 ...
- Docker——网络和存储(数据卷)
iptables -t nat -vnL |grep docker 查看docker桥接网卡:brctl show 本地端口随机映射到docker容器的80端口上: docker run -d -P ...
- 牛客 132C 简单瞎搞题 (bitset)
大意: 给定序列$a$的第$i$个元素的取值范围$[L_i,R_i]$, 求$a$的平方和的种类数. 用bitset优化, 复杂度$O(\frac{n^5}{\omega})$ #include &l ...
- arcgis之隐藏设置放大缩小按钮
arcgis之隐藏设置放大缩小按钮 隐藏按钮: view.ui._removeComponents(['zoom']) 设置按钮: let zoom = new Zoom({ view: this.v ...
- otter+canal
https://blog.csdn.net/u011142688/article/details/52046928 https://blog.csdn.net/chenzeyuczy/article/ ...
- Sql Server 导出数据库表结构的SQL查询语句
--导出数据库所有表 SELECT 表名 Then D.name Else '' End, 表说明 Then isnull(F.value,'') Else '' End, 字段序号 = A.colo ...
- golang 环境变量讲解
以下配置以MAC 下配置为例,但其他环境下大同小异. GOROOT就是go的安装路径在~/.bash_profile中添加下面语句: GOROOT=/usr/local/go export GOROO ...