• 浅谈可持久化线段树(主席树)


    Ⅰ、前言

    感谢洛谷孤独·粲泽大佬写的题解,对我启发至深。

    Ⅱ、预备知识

    可持久化线段树,顾名思义,是建立在线段树的基础之上的,一般来说,可持久化线段树具备两种特性:
    1、可以访问历史版本(即可以在之前的操作基础上询问或修改)
    2、可以在历史版本的基础之上进行修改操作
    明白了这些基础知识,接下来我们就可以来学习可持久化线段树了

    Ⅲ、抛出问题

    我们先来看一道洛谷的模板题

    题目描述

    如题,你需要维护这样的一个长度为(N)的数组,支持如下几种操作
    在某个历史版本上修改某一个位置上的值
    访问某个历史版本上的某一位置的值
    此外,每进行一次操作(对于操作2,即为生成一个完全一样的版本,不作任何改动),就会生成一个新的版本。版本编号即为当前操作的编号(从1开始编号,版本0表示初始状态数组)

    输入输出格式

    输入格式:

    输入的第一行包含两个正整数(N)(M)分别表示数组的长度和操作的个数。
    第二行包含(N)个整数,依次为初始状态下数组各位的值(依次为(a_{i}),(1leq ileq N))。
    接下来(M)行每行包含3或4个整数,代表两种操作之一((i)为基于的历史版本号):
    对于操作1,格式为(v_{i}) (1) (loc_{i}) (value_{i}), 即为在版本(v_{i})的基础上,(a_{loc_{i}})修改为(value_{i})
    对于操作2,格式为(v_{i}) (2) (loc_{i}),即访问版本(v_{i})中的(loc_{i})的值

    输出格式:

    输出包含若干行,依次为每个操作2的结果。

    输入输出样例

    输入样例#1:

    5 10
    59 46 14 87 41
    0 2 1
    0 1 1 14
    0 1 1 57
    0 1 1 88
    4 2 4
    0 2 5
    0 2 4
    4 2 1
    2 2 2
    1 1 5 91

    输出样例#1:

    59
    87
    41
    87
    88
    46

    说明:

    (1leq N,Mleq10^6)

    Ⅳ、分析问题

    可持久化线段树,其特点在于,可以访问历史版本的值(上面讲了),那么如何记录历史版本呢?
    最朴素的思想就是,我们每遇到一次操作,就把上次的线段树复制下来,再在复制后的线段树上进行操作,相当于每有一次操作,就新建一个线段树,并记录每棵线段树的根节点编号,每次访问时访问某棵线段树即可。
    可仔细分析复杂度后,发现时间复杂度远大于正常范围,空间更是开不下,直接新建新线段树这个方法算是废了,能不能在原来的基础上改进呢?
    分析下图,第0棵线段树(红色为点权值,绿色为其覆盖区间)

    第1棵线段树是下图↓

    我们发现,第一棵线段树与初始线段树(我们暂且在这里怎么称呼它)几乎一模一样,如果把原来的节点都复制一遍,相当于多开了许多空间,而初始线段树的节点权却没有发挥作用。
    怎么解决呢?
    这里引入一个新操作,可能我们的新线段树可以和历史版本的线段树共用一段空间,因为每次线段树的改动是极为微小的,只改动了(log_{2}{n})的节点,剩下的节点并没有修改,此时我们即可将未改动部分和历史版本的这一部分共用一段空间
    如图,第1棵线段树与第0棵线段树共用了2~9这一段空间,在节点数量非常多的情况下,这个操作能取得很明显的效果。

    第二次操作,将一号节点的值加14

    接下来就是代码(下面有详细的注释哦):

    #include<bits/stdc++.h>
    #define ll long long
    #define INF 2147483647
    #define mem(i,j) memset(i,j,sizeof(i))
    #define F(i,j,n) for(register int i=j;i<=n;i++)
    using namespace std;
    int n,m,pntnum=0;//pntnum表示一共建立过多少点
    int v[20000010],root[20000010];//v为输入的点权,root[i]表示第i棵线段树的根节点
    inline int read(){//快读没有什么问题
    	int datta=0;char chchc=getchar();bool okoko=0;
    	while(chchc<'0'||chchc>'9'){if(chchc=='-')okoko=1;chchc=getchar();}
    	while(chchc>='0'&&chchc<='9'){datta=datta*10+chchc-'0';chchc=getchar();}
    	if(okoko==1)return -datta;return datta;
    }
    struct Persistable_Segment_Tree{
    	int ls[20000010],rs[20000010],tree[20000010];
    	//ls[i]为i的左儿子,rs[i]为i的右儿子,tree[i]为i的值
    	void build_tree(int &pos,int l,int r){
    		pos=++pntnum;//添加新节点,当前节点编号为++pntnum
    		if(l==r){
    			tree[pos]=v[l];//初始化每个叶节点的值
    			return ;
    		}
    		int mid=(l+r)>>1;
    		build_tree(ls[pos],l,mid);
    		build_tree(rs[pos],mid+1,r); 
    	}
    	void add(int &pos,int vsn,int l,int r,int loc,int val){//pos新版本的当前节点编号,vsn旧版本的当前节点编号,l左端点,r右端点,loc要修改的节点编号,val修改值
    		pos=++pntnum;//新建节点
    		if(l==r){
    			tree[pos]=val;//修改值
    			return ;
    		}
    		ls[pos]=ls[vsn];//继承旧版本左子树
    		rs[pos]=rs[vsn];//继承旧版右左子树
    		int mid=(l+r)>>1;
    		if(loc<=mid)//如果要修改的节点在左子树中
    			add(ls[pos],ls[vsn],l,mid,loc,val);//处理左子树
    		else
    			add(rs[pos],rs[vsn],mid+1,r,loc,val);//处理右子树
    	}
    	int ask(int vsn,int l,int r,int loc){//vsn要访问的版本的当前节点编号,l左端点,r右端点,loc要访问的节点编号
    		if(l==r)
    			return tree[vsn];
    		int mid=(l+r)>>1;
    		if(loc<=mid)//如果在左子树中
    			return ask(ls[vsn],l,mid,loc);
    		else
    			return ask(rs[vsn],mid+1,r,loc);
    	}
    }T;
    int main(){
    	n=read();m=read();
    	F(i,1,n)
    		v[i]=read();
    	T.build_tree(root[0],1,n);//建树
    	F(i,1,m){	
    		int rt=read(),kd=read(),loc=read(),val;//rt历史版本编号,kd操作类型,loc节点编号,val修改后的值
    		if(kd==1){
    			val=read();
    			T.add(root[i],root[rt],1,n,loc,val);
    		}else{
    			root[i]=root[rt];//root[i]是第i个版本的根节点编号,因为没有修改,所以只需要继承以前版本
    			printf("%d
    ",T.ask(root[rt],1,n,loc));
    		}
    	}
    	return 0;
    }
    

    区间第k小是可持久化线段树的一个经典应用,如题

    题目描述

    如题,给定(N)个正整数构成的序列,将对于指定的闭区间查询其区间内的第(k)小值。

    输入输出格式

    输入格式:

    第一行包含两个正整数(N)(M),分别表示序列的长度和查询的个数。
    第二行包含(N)个正整数,表示这个序列各项的数字。
    接下来M行每行包含三个整数(l,r,k),表示查询区间(left[l,r ight])内的第(k)小值。

    输出格式:

    输出包含(k)行,每行1个正整数,依次表示每一次查询的结果

    输入输出样例

    输入样例#1:

    5 5
    25957 6405 15770 26287 26465
    2 2 1
    3 4 1
    4 5 1
    1 2 2
    4 4 1

    输出样例#1:

    6405
    15770
    26287
    25957
    26287
    区间第k小问题可以有许多其他写法,我们这里尝试使用可持久化线段树解题。

    区间第k小(无修改操作):

    #include<bits/stdc++.h>
    #define ll long long
    #define mem(i,j) memset(i,j,sizeof(i))
    #define F(i,j,n) for(register int i=j;i<=n;i++)
    using namespace std;
    struct hahaha{
    	int v,id;
    }s[4000010];
    int n,m,pntnum=0,a[4000010];
    int root[4000010],b[4000010];
    inline int read(){
    	int datta=0;char chchc=getchar();bool okoko=0;
    	while(chchc<'0'||chchc>'9'){if(chchc=='-')okoko=1;chchc=getchar();}
    	while(chchc>='0'&&chchc<='9'){datta=datta*10+chchc-'0';chchc=getchar();}
    	if(okoko==1)return -datta;return datta;
    }
    inline bool cmp(hahaha a,hahaha b){
    	return a.v<b.v;
    }
    struct Persistable_Segment_Tree{
    	int ls[4000010],rs[4000010],tree[4000010];
    	void build_tree(int &pos,int l,int r){
    		pos=++pntnum;
    		if(l==r)
    			return ;
    		int mid=(l+r)>>1;
    		build_tree(ls[pos],l,mid);
    		build_tree(rs[pos],mid+1,r);
    	}
    	void add(int &pos,int vsn,int l,int r,int loc){//pos新版本的当前节点编号,vsn旧版本的当前节点编号,l左端点,r右端点,loc要修改的节点编号
    		pos=++pntnum;
    		if(l==r){
    			tree[pos]=tree[vsn]+1;//当前节点加1
    			return ;
    		}
    		ls[pos]=ls[vsn];//继承左子树
    		rs[pos]=rs[vsn];//继承右子树
    		tree[pos]=tree[vsn]+1;//当前路径上的所有点权值加1
    		int mid=(l+r)>>1;
    		if(loc<=mid)
    			add(ls[pos],ls[vsn],l,mid,loc);
    		else
    			add(rs[pos],rs[vsn],mid+1,r,loc);
    	}
    	int ask(int lv,int rv,int l,int r,int k){
    		if(l==r)
    			return l;//返回第l小
    		int mid=(l+r)>>1,sum=tree[ls[rv]]-tree[ls[lv]];//sum为第rv棵树的
    		if(k<=sum)
    			return ask(ls[lv],ls[rv],l,mid,k);
    		else
    			return ask(rs[lv],rs[rv],mid+1,r,k-sum);
    	}
    }T;
    int main(){
    	n=read();m=read();
    	F(i,1,n)
    		s[i].v=read(),s[i].id=i;
    	sort(s+1,s+n+1,cmp);//将节点按权值排序
    	int num=0;
    	s[0].v=-0x3f3f3f3f;
    	F(i,1,n)
    		a[s[i].id]=(s[i].v!=s[i-1].v)?++num:num;//将节点权值离散化
    	int nowi=1,numi=1;
    	while(numi<=n){
    		if(s[numi].v!=s[numi-1].v)
    			b[nowi++]=s[numi].v;//将离散化后的数组每个数只出现一次地放到b数组中,便于最后输出第几小
    		numi++;
    	}
    	T.build_tree(root[0],1,nowi);//建空树
    	F(i,1,n)
    		T.add(root[i],root[i-1],1,n,a[i]);//加点,详见add函数
    	F(i,1,m){
    		int lft=read(),rht=read(),kn=read();
    		printf("%d
    ",b[T.ask(root[lft-1],root[rht],1,n,kn)]);
    	}
    	return 0;
    }
    
  • 相关阅读:
    Code First 迁移----官方 应用程序启动时自动升级(MigrateDatabaseToLatestVersion 初始值设定项)
    使用 Entity Framework Core 时,通过代码自动 Migration
    泛型反射获取特性值
    使用Expression动态创建lambda表达式
    ElasticSearch + Logstash + Kibana 搭建笔记
    PHP获取指定函数定义在哪个文件中及行号
    Wordpress中文章的特色图像Featured Image究竟存在哪里?
    PhpSpreadsheet生成Excel时实现单元格自动换行
    Composer使用体验
    PHP命名空间学习笔记
  • 原文地址:https://www.cnblogs.com/hzf29721/p/9466930.html
Copyright © 2020-2023  润新知