首页 > 其他分享 >第二课——线段树

第二课——线段树

时间:2024-03-23 16:00:26浏览次数:19  
标签:rt 第二课 int 线段 数组 区间 data

上一节课讲了树状数组,也介绍了树状数组的优点与不足,这里简单回顾一下。
优点:树状数组的代码非常简短,易于实现,被刘老师亲切的称为IO选手的"HelloWorld!",就是因为代码短。
缺点:树状数组的缺点也非常的明显,只能处理单点修改区间查询或者区间修改单点查询的问题(以较高的效率)。而区间修改区间查询的问题没有办法很优雅的解决,于是引出了线段树。

线段树

先来看一个问题:


7-1 张煊的金箍棒(2)
张煊的金箍棒升级了!

升级后的金箍棒是由几根相同长度的金属棒连接而成(最开始都是铜棒,从1到N编号);

张煊作为金箍棒的主人,可以对金箍棒施以任意的变换,每次变换操作就是将一段连续的金属棒(从X到Y编号)改为铜棒,银棒或金棒。

金箍棒的总价值计算为N个金属棒的价值总和。其中,每个铜棒价值为1;每个银棒价值为2;每个金棒价值为3。

现在,张煊想知道多次执行操作后的金箍棒总价值。
输入格式:

输入的第一行是测试数据的组数(不超过10个)。

对于每组测试数据,第一行包含一个整数N(1 <= N <= 100000),表示金箍棒有N节金属组成,第二行包含一个整数Q(0 <= Q <= 100,000),表示执行变换的操作次数。

接下来的Q行,每行包含三个整数X,Y,Z(1 <= X <= Y <= N,1 <= Z <= 3),它定义了一个操作:将从X到Y编号的金属棒变换为金属种类Z,其中Z = 1代表铜棒,Z = 2代表银棒,Z = 3代表金棒。
输出格式:

对于每组测试数据,请输出一个数字,表示操作后金箍棒的总价值。

每组数据输出一行。
输入样例:

1
10
2
1 5 2
5 9 3

输出样例:

24

可以看到题目中非常明显的区间修改+区间查询的意图,这也是线段树的一道入门题目。接下来我来介绍这个神奇的数据结构。

构成

线段树由一个四倍原数组长的数组组成,对于数组中的元素也有着特殊的含义,但是比起树状数组来说要好理解多了。
首先我们不从数组层面来看这个数据结构,而是从一个二叉树,一棵完全二叉树。假设我们的原数组有8个数。那么线段数和原数组的关系就像这样:
image
在线段树上的每一个节点表示对应区间的某个属性值,只要这个属性值满足区间的加法即可。
举个例子,这个属性值可以是区间和,可以是区间最值等等,这些具体的属性由题目来决定了,由于属性值的自由度极高,导致线段树在非常多的场合可以用于加速。

见过了线段树的二叉树形状,接下里给线段树一个数组的表示方式。这个也非常的简单:
image
和《数据结构》中一致,从根节点开始为 1 ,宽度优先搜索的顺序升序标号。有了标号,我们就能用数组来存储这棵二叉树了。可是为什么我们需要四倍的原数组空间呢

这里我们从长度为5的数组开始,来探讨一下这个问题。
image
原数组长度为5,那么理论上黑色的节点已经够用了,但是我们使用的静态数组,一般会选择直接把完全二叉树所需的空间开出来,所以会用到最多四倍的空间。

左右子结点的访问

学过《数据结构》的读者可以跳过这块内容。
这部分比较简单,假设根的标志是1,那么左右子结点分别可以用以下两个函数访问:

int left(int d){
	return data[d<<1];
}
int right(int d){
	return data[d<<1|1];	//等价于 d * 2 + 1
}

稍微解释一下访问右节点的操作,一个二进制数在左移动后最低位一定是0,那么这时候可以用1与该数位或,就能得到乘2加一的效果。

树的初始化

首先定义一下线段树的结构(代码层面)

class SeqTree{
public:
	//方法定义
private:
	struct Data{
		int val;
	}data[N<<2];
}seqTree;			//线段树类
int arr[N];			//原数组

树的初始化是从原数组构造我们的线段树,以前面提到的题目为例子。节点的属性是区间和

//arr为原数组、l为区间左边、r为区间右边、rt为线段树上的位置
void build(int*arr,int l,int r,int rt){
	if(l==r){							//到了叶子节点,直接赋值
		data[rt].val = arr[l-1];
	}
	int m = (l+r)>>1;					//寻找左右子结点的区间边界
	build(arr,l,m,rt<<1);				//递归构造两边的线段树
	build(arr,m+1,r,rt<<1|1);
	pushUp(rt);							//利用两边的子节点更新当前节点
}
inline void pushUp(int rt){
	data[rt].val = data[rt<<1].val + data[rt<<1|1].val;
}

可以发现线段树的构造是非常容易理解的。由于二分的存在它的复杂度也只是O(NlogN)。

单点修改

线段树的修改,相当于修改最下层的某个节点,它会影响到上层的非常多节点,依照树的初始化的想法,我们可以很容易的写出修改代码,这里不提供。

区间查询

首先有一个理论保障:线段树的每次查询不会超过O(logN)的复杂度。为什么呢?

  • 任一连续区间至多由\(2log_2^N\)个子区间组成
    • 原因:任一区间不在线段树同一层出现两个子区间,并且树高不超过\(logN\)
      • 原因的原因:因为区间连续,所以如果在同一层出现了两个子区间,那么这两个子区间一定可以合成上一层的一个区间。

所以查询的复杂度有了保障。于是我们来讲查询的思路。
对于一个区间查询\([L,R]\),我们从根节点[0,4N]出发,进行二分查找,并把符合要求的区间上的节点都进行修改。Idea is pool show me the code!

//区间查询[R,L]
int query(int R,int L,int r,int l,int rt){
	if(R>l||L<r)return 0;
	if(R<=r&&L>=l)return data[rt].val;
	int m = (l+r)>>1;
	return query(R,L,r,m,rt<<1)+query(R,L,m+1,l,rt<<1|1);
}

是不是超级简单?哈哈,刘老师说:“当年我们没有人教,没有题目刷的时候,学会了线段树就开始大杀四方,当时觉得是很稀奇的东西。你们今天倒好,随便就能学到如此有意思的算法。”

区间修改

这个就厉害了,不仅实现了区间修改,还引入了最高效的偷懒方式——lazy
思路是这样的:我们修改一个区间的时候,如果要把值给到每个受影响的节点,会非常的麻烦,并且涉及到多次修改时,程序的复杂度会较高。但是仔细想想,我们线段树上的节点不是能代表属性么,那是不是也可以记录修改的属性呢?于是lazy诞生了。
修改一个区间的时候,我们不修改对应的叶子节点,而是在最上层的区间节点上记录本次修改,并在查询的时候应用。
我们首先修改一下数据结构体:

class SeqTree{
public:
	//方法定义
private:
	struct Data{
		int val;
		int lazy;	//lazy标志
	}data[N<<2];
}seqTree;			//线段树类
int arr[N];			//原数组

然后重写之前的各个方法:

void build(int*arr,int l,int r,int rt){
	if(l==r){
		data[rt].val = arr[l-1];
	}
	int m = (l+r)>>1;
	data[rt].lazy = 0;										//给lazy初始化值
	build(arr,l,m,rt<<1);
	build(arr,m+1,r,rt<<1|1);
	pushUp(rt);
}
//区间查询[R,L]
int query(int R,int L,int r,int l,int rt){
	if(R>l||L<r)return 0;
	if(R<=r&&L>=l)return data[rt].val;
	int m = (l+r)>>1;
	pushDown(rt,m-r+1,l-m);									//新加了一个应用lazy的函数
	return query(R,L,r,m,rt<<1)+query(R,L,m+1,l,rt<<1|1);
}
//区间修改把[R,L]修改为C
void update(int R,int L,int C,int r,int l,int rt){
	if(R>l||L<r){
		return;
	}
	if(R<=r&&L>=l){
		data[rt].val=C*(l-r+1);								//更新节点值
		if(r<l)
			data[rt].lazy=C;								//查询到此,继承lazy值
		return;
	}
	int m = (l+r)>>1;
	pushDown(rt,m-r+1,l-m);									//应用lazy
	update(R,L,C,r,m,rt<<1);
	update(R,L,C,m+1,l,rt<<1|1);
	pushUp(rt);												//这里有一个细节,应用lazy要在向上计算value之前
}

下面是应用lazy的函数的实现:

inline void pushDown(int rt,int rn,int ln){
	if(data[rt].lazy){
		data[rt<<1].val=data[rt].lazy*rn;
		data[rt<<1].lazy=data[rt].lazy;
		data[rt<<1|1].val=data[rt].lazy*ln;
		data[rt<<1|1].lazy=data[rt].lazy;
		data[rt].lazy=0;
	}
}

到这里就讲完了,线段树我似乎没有进行多少理论的分析,大部分都是show you the code.但是线段树是一个抽象的,强大的优化工具,而不是一个算法。想要理解线段树,还需要自己去编码实现。这里提供完整的程序代码供你参考。

点击查看代码
#include <iostream>
#include <algorithm>
#include <vector>
#include <string>
#include <string.h>
using namespace std;

#define N 100000

class SeqTree{
public:
	inline void clear(int size){
		memset(data,0,sizeof(Data)*(size<<2));
		for(int i=1;i<=(size<<2);i++){
			data[i].val = 1;
		}
	}
	inline void pushUp(int rt){
		data[rt].val = data[rt<<1].val + data[rt<<1|1].val;
	}
	inline void pushDown(int rt,int rn,int ln){
		if(data[rt].lazy){
			data[rt<<1].val=data[rt].lazy*rn;
			data[rt<<1].lazy=data[rt].lazy;
			data[rt<<1|1].val=data[rt].lazy*ln;
			data[rt<<1|1].lazy=data[rt].lazy;
			data[rt].lazy=0;
		}
	}
	void build(int*arr,int l,int r,int rt){
		if(l==r){
			data[rt].val = arr[l-1];
		}
		int m = (l+r)>>1;
		data[rt].lazy = 0;
		build(arr,l,m,rt<<1);
		build(arr,m+1,r,rt<<1|1);
		pushUp(rt);
	}
	//区间查询[R,L]
	int query(int R,int L,int r,int l,int rt){
		if(R>l||L<r)return 0;
		if(R<=r&&L>=l)return data[rt].val;
		int m = (l+r)>>1;
		pushDown(rt,m-r+1,l-m);
		return query(R,L,r,m,rt<<1)+query(R,L,m+1,l,rt<<1|1);
	}
	void update(int R,int L,int C,int r,int l,int rt){
		if(R>l||L<r){
			return;
		}
		if(R<=r&&L>=l){
			data[rt].val=C*(l-r+1);
			if(r<l)
				data[rt].lazy=C;
			return;
		}
		int m = (l+r)>>1;
		pushDown(rt,m-r+1,l-m);
		update(R,L,C,r,m,rt<<1);
		update(R,L,C,m+1,l,rt<<1|1);
		pushUp(rt);
	}
	void debug(int size){
		cout<<"############## debug ##############\n";
		for(int i=1;i<=(size<<2);i++){
			cout<<data[i].val<<" "<<data[i].lazy<<"\n";
		}
		cout<<"############## debug ##############\n";

	}
private:
	struct Data{
		int val;
		int lazy;
	}data[N<<2];
}seqTree;

int main(){
	int b,n,q;
	cin>>b;
	while(b--){
		cin>>n>>q;
		seqTree.clear(n);
		int x,y,z;
		for(int i=0;i<q;i++){
			cin>>x>>y>>z;
			seqTree.update(x,y,z,1,n,1);
		}
		cout<<seqTree.query(1,n,1,n,1)<<"\n";
		// seqTree.debug(n);
	}
}

好好领悟线段树的节点属性吧。

标签:rt,第二课,int,线段,数组,区间,data
From: https://www.cnblogs.com/zhywyt/p/18091071

相关文章

  • CF938G-动态连通图最短xor路(线段树分治、并查集、线性基)
    link:https://codeforces.com/contest/938/problem/G[!Description]有一张连通的无向图简单图,三种操作:1、加边\((x,y,d)\)2、删边\((x,y)\)3、询问\(x\toy\)的路径中异或最小的路径(不一定是简单路径)保证每次操作后图是连通的\(1\leqn,m,q\leq2\times10^5\).这......
  • 【Git】第二课:git安装和配置
    ......
  • 楼房重建线段树
    link维护单调序列、点修的线段树。首先考虑一个楼房被看见的必要条件是前面没有斜率大于它的楼房,不等式可以推出来。然后本质上是维护一个从\(1\)号开始斜率单调上升的序列长度,注意,不能跳跃选择,即某栋楼房能加入序列则必须加入。线段树维护区间最大值和区间上升序列长度,由于......
  • Redis第二课,1.set key value(设置对应的key和value)2.get key(得到value值)Redis全局
    Redis的启动 redis-cli目录1.setkeyvalue(设置对应的key和value)2.getkey(得到value值)Redis全局命令(支持很多的数据结构)3.keys(用来查询当前服务器匹配的key)生产环境/线上环境4.exist(判定key是否存在):判定key是否存在​编辑5.DEL  key 返回删掉的key......
  • 李超线段树
    模板题动态添加线段,求某个\(x\)对应的\(y\)最大是多少,且对应哪条直线。因为\(x\)比较小,考虑在\(x\)轴上建立线段树。把每个线段写成\(y=kx+b\)的解析式形式并求出它的定义域\([l,r]\),每条线段就可以看作是一个应用在\([l,r]\)上的区间修改。每次查询就是单点查询。......
  • P1712 [NOI2016] 区间 线段树+双指针
    //Problem://P1712[NOI2016]区间////Contest:Luogu//URL:https://www.luogu.com.cn/problem/P1712//MemoryLimit:250MB//TimeLimit:1000ms////PoweredbyCPEditor(https://cpeditor.org)#include<iostream>#include<algorithm......
  • CF1514D-区间(绝对)众数-莫队、随机化、可持久化线段树
    link:https://codeforces.com/contest/1514/problem/D很久以前小号打的场了,当时D题写的莫队,现在重新来看看。题意:给一个序列\([a_1,\dots,a_n]\),有q次询问,每次问:把\([a_l,\dots,a_r]\)划分最少几个不相交子序列,才能使得每个子序列是beautiful的。称一个序列\(a_1,\dots,a_x\)......
  • 大都市meg(线段树/树状数组+LCA)
    题目描述在经济全球化浪潮的影响下,习惯于漫步在清晨的乡间小路的邮递员BlueMary也开始骑着摩托车传递邮件了。不过,她经常回忆起以前在乡间漫步的情景。昔日,乡下有依次编号为1..n的n个小村庄,某些村庄之间有一些双向的土路。从每个村庄都恰好有一条路径到达村庄1(即比特堡)。并且......
  • 线段树模板
    线段树模板沉淀了很久,总算是掌握了线段树的经典模板了。但是还是需要深刻练习才能反复加深印象呢//线段树模板#include<bits/stdc++.h>usingnamespacestd;#defineintlonglongconstintN=1e5+10;intans[N*4];intt1[N*4],t2[N*4];inta[N];//存放输入数据intn,......
  • Windows powershell的初步学习使用第二课
           今天我们来学习Windowspowershell的指令。       上指令(参数为cmdlet):get-executionPolicy        作用为查询当前执行策略。       结果有以下几种情况:Restricted:脚本不能运行(默认设置)RemoteSigned:在本地创建脚本可以运行,但从......