一篇关于线段树是什么树的博客

本来是想做一篇解题报告的。。后来发现说这么多跟题一点关系都没有。。
这是一篇比较适合一个线段树初学者的文章
先看一道题感受一下线段树能做什么
题目FZOJ P3401 由于我非常良心以及FZOJ很省字符 优化的题面如下:
题目描述:
如题,已知一个数列,你需要进行下面两种操作:

1.查询第x个数和第y个数之间最小值为多少

2.修改第X个数为Y

输入格式:
第一行包含两个整数N、M,分别表示该数列数字的个数和操作的总个数。

第二行包含N个用空格分隔的整数,其中第i个数字表示数列第i项的初始值。

接下来M行每行包含3个整数,表示一个操作,具体如下:

操作1: 格式:1 x y 含义:输出区间[x,y]内最小值是多少

操作2: 格式:2 x y 含义:修改第x个数的值为y

输出格式:
输出包含若干行整数,即为所有操作1的结果。
样例输入:
10 3
1 2 3 4 5 6 7 8 9 10
1 2 7
2 2 0
1 1 10
样例输出:
2
0
说明:
时空限制:1000ms,128M

数据规模:
对于100%的数据满足:1<=N,M,x,y<=100000

既然是一道线段树裸题 那就直接讲讲入门级别的线段树好了 毕竟我这么菜也不会更难的了

什么是线段树

线段树是一种非常实用的数据结构 可以维护很多有用的东西 比如它可以维护区间最大值最小值 区间和等
先粘一波图 图是在大哥那里抄来的

用这个就比较好说了
这就是一个1-10的线段树结构 废话
线段树就是将一个区间变成一棵帅气的树
暴力做法可以用 O ( n ) O(n) O(n)的时间进行修改 用 O ( 1 ) O(1) O(1)的时间进行修改。注意到线段树是一颗一个爹有俩儿子的二叉树 可以采用二分的思想查询和修改 这样复杂度就都是 O ( l o g n ) O(logn) O(logn)相对更优一点
通过上图 我们可以发现 如果一个序列长度为 n n n 那么它所需要的线段树大小是 2 n 2n 2n 不过由于毒瘤出题人会把线段树卡成一条链…所以开 4 4 4倍空间防止 R E RE RE。有时受到空间限制要离散化 动态开点 有兴趣的可以自己研究研究

如何存储线段树

在讲操作之前 我们先讲一下如何存储线段树 操作学的再好不会存有个毛用
还是这个图
在这里插入图片描述
我们将最上面最大的那个节点编号为 1 1 1,左儿子是 2 2 2,右儿子是 3 3 3,以此类推
存储这个区间用到了结构体
以维护区间最大值为例 存储线段树的代码如下:
在这里插入图片描述
恭喜你 你已经会了一道线段树题的 4 4 4 / 90 /90 /90行 了!

线段树怎么建

刚才我们成功地学会了如何存储线段树 那么怎么建一个线段树呢
还是用上图说话
在这里插入图片描述
我们要建一棵树 就要先从爹开始建 但我们发现我们无法求出爹想要维护的信息 所以我们先把他的儿子建好 建好之后通过儿子维护的信息我们推出爹维护的信息
我们以维护区间最大值为例 我们想知道爹的最大值 就可以通过两个儿子区间最大值的最大值来求出爹区间的最大值
具体细节看代码:
在这里插入图片描述

这里说一嘴 l s ls ls指的是节点的左儿子 ( l e f t s o n ) (leftson) (leftson) r s rs rs指的是节点的右儿子 ( r i g h t s o n ) (rightson) (rightson)
写代码时在上面加上这个就好啦
在这里插入图片描述

如何操作线段树

上面已经说了 通过线段树可以用一棵树来搞一个区间
通常操作线段树我们都用递归的方法 先将一个区间分成若干个区间 再通过二分查找到要操作的区间 进行修改
什么?听不懂?我要是第一次学也听不懂 那我们来的直观一点
再把大哥的图抄来

比如我们要操作 [ 4 , 8 ] [4,8] [4,8]这个区间
我们先找到 [ 1 , 10 ] [1,10] [1,10],我们把 [ 1 , 10 ] [1,10] [1,10]这个区间拆分为 [ 1 , 5 ] [1,5] [1,5], [ 6 , 10 ] [6,10] [6,10],发现 [ 1 , 10 ] [1,10] [1,10] m i d = 5 mid=5 mid=5 4 4 4的右边 8 8 8的左边, 则这两个区间都要查找。
先看 [ 1 , 5 ] [1,5] [1,5] (右边就不看了它的 m i d = 3 mid=3 mid=3 3 3 3 4 4 4的左边 所以只需要查找它的右儿子 [ 4 , 5 ] [4,5] [4,5]然后 [ 3 , 5 ] [3,5] [3,5] m i d = 4 mid=4 mid=4 4 4 4的右边, 5 5 5的左边 所以要查找 [ 4 , 4 ] [ 5 , 5 ] [4,4][5,5] [4,4][5,5]这时 我们惊人的发现!我们找到了两个长度为 1 1 1的区间 这就比较好操作了,因为节点只有对应的数,这时我们再回溯回去。
具体细节看代码—这里还是以单点修改 维护区间最大值做例子:
在这里插入图片描述
区间查询的操作?
在这里插入图片描述
所以一道单点修改区间求最大值的线段树就这样结束了
附上代码

#include<Cstdio>
#include<iostream>
using namespace std;
#define ls k<<1
#define rs k<<1|1
#define N (int)(1e5+1)
inline void read(int &x)
{
    
    
	int s=0,w=1;char ch=getchar();
	while(ch<'0'||ch>'9'){
    
    if(ch=='-')w=-1;ch=getchar();}
	while(ch>='0'&&ch<='9'){
    
    s=(s<<3)+(s<<1)+(ch&15);ch=getchar();}
	x=s*w;
}//快速输入优化
void print(int x)
{
    
    
	if(x<0){
    
    putchar('-');x=-x;}
	putchar(x%10+'0');
	print(x/10);
}//快速输出优化
struct node
{
    
    
	int l;  //l存储区间的左端点
	int r;  //r存储区间的右端点
	int maxx;//maxx维护区间的最大值
}t[N<<2];//上面有提到 线段树要开4倍空间
int n,m,opr,x,y,a[N];
void build(int k, int l, int r)//k表示现在建到了线段树的哪个区间
{
    
    
	//l表示要建的左端点 r表示要建的右端点
	t[k].l=l;t[k].r=r;//先把这个节点的左右端点赋值
	if(l==r)//如果建的这个区间左右端点相等
	{
    
    
		//说明此时我们找到的区间长度为1 即序列中的一个数
		t[k].maxx=a[l];//赋值 这个区间中最大的数就是它自己
		return;
	}
	int mid=l+r>>1;//mid为区间中点 二分向下建区间
	build(ls,l,mid);
	build(rs,mid+1,r);
	t[k].maxx=max(t[ls].maxx,t[rs].maxx);
	//k区间的最大值就是两个儿子的区间最大值
	//这一步叫标记上传 以后做更高级的题时可能会写成一个函数
	//但每步操作下面不要忘了加上函数 曾经忘了这句话调了40分钟...
}
void modify(int k, int x, int v)//k表示目前节点的编号
{
    
    
	//x表示要修改第x个数 v表示要将区间修改为v这个值
	if(t[k].l==t[k].r)//找到了叶子节点,直接修改
	{
    
    
		t[k].maxx=v;
		return;
	}//找到叶子节点k则一定有t[k].l=t[k].r=x
	int mid=t[k].l+t[k].r>>1;
	if(x<=mid)modify(ls,x,v);//如果x在mid的左边说明要找左儿子
	else modify(rs,x,v);//否则找右儿子
	t[k].maxx=max(t[ls].maxx,t[rs].maxx);//别忘记上传!!!
}
int query(int k, int l, int r)
{
    
    
	if(t[k].l==l&&t[k].r==r)//如果编号为k的节点刚好是所求区间
		return t[k].maxx;//直接返回该区间的最大值
	int mid=t[k].l+t[k].r>>1;
	if(mid<l)return query(rs,l,r);//如果左儿子不在所查询的区间内找右儿子
	else if(mid>=r)return query(ls,l,r)//右儿子不在区间内找左儿子
	else return max(query(ls,l,mid),query(rs,mid+1,r));
	//左右儿子都有在区间内的查两个区间的最大值求爹的最大值
}
int main()
{
    
    
	read(n);read(m);
	for(int i=1;i<=n;i++)read(a[i]);
	build(1,1,n);//上次考试20分钟调试后发现没打build...一定不要忘记build
	//从第一个节点建树 建的范围是[1,n]
	for(int i=1;i<=m;i++)
	{
    
    
		read(opr);read(x);read(y);
		if(opr==1)print(query(x,y));
		else modify(1,x,y);//从第一个节点开始搜索 将第x个数改为y
	}
}

你以为完事了吗??并没有!这只是开始【奸笑】
来看一下这道题
luoguP3372【模板】线段树 1
题目描述:
如题,已知一个数列,你需要进行下面两种操作:

1.将某区间每一个数加上x

2.求出某区间每一个数的和
输入格式:
第一行包含两个整数N、M,分别表示该数列数字的个数和操作的总个数。

第二行包含N个用空格分隔的整数,其中第i个数字表示数列第i项的初始值。

接下来M行每行包含3或4个整数,表示一个操作,具体如下:

操作1: 格式:1 x y k 含义:将区间[x,y]内每个数加上k

操作2: 格式:2 x y 含义:输出区间[x,y]内每个数的和

输出格式:
输出包含若干行整数,即为所有操作2的结果。
样例输入:
5 5
1 5 4 2 3
2 2 4
1 2 3 2
2 3 4
1 1 5 1
2 1 4
样例输出:
11
8
20
说明:
数据规模:

对于30%的数据:N<=8,M<=10

对于70%的数据:N<=1000,M<=10000

对于100%的数据:N<=100000,M<=100000

(数据已经过加强_,保证在int64/long long数据范围内)

懒标记

懒标记 懒标记 顾名思义 老子能不干的事就不干
多用于区间修改
我们在进行查询操作时 只需要知道当前子树的状态 并不需要知道所有子树的状态 懒标记的作用就是先存储下来要对子树做的事情 等到查询时再将标记下传
比如这道区间加法→_→ 我们在进行加法修改的时候不用将所有的区间都修改 只需要将在区间里的爹修改 并打上懒标记 在查询时再将懒标记下传 这样就能省下很多时间

需要注意的细节

  1. 线段树区间修改时,注意判断有新的标记的时候再改,这样能避免很多无用的+0-0之类的操作,速度快非常多。
  2. 在懒标记有多个时 要注意优先级,先想好下传哪个懒标记对下传其他懒标记没有影响再进行操作。如区间加法乘法 要先下传乘法懒标记再下传加法懒标记。

本题代码如下:(能看到这里的都比我强 所以就不写注释了 其实是我懒 )

#include<cstdio>
#define ls k<<1
#define rs k<<1|1
typedef long long ll;
inline void read(ll &x)
{
    
    
	ll s=0,w=1;char ch=getchar();
	while(ch<'0'||ch>'9'){
    
    if(ch=='-')w=-1;ch=getchar();}
	while(ch>='0'&&ch<='9'){
    
    s=(s<<3)+(s<<1)+ch-'0';ch=getchar();}
	x=s*w;
}
ll n,m,p,q,r,s,a[100002];
struct node
{
    
    
	ll l,r,w,lzy;
}t[400004];
void pushup(ll k){
    
    t[k].w=t[ls].w+t[rs].w;}
void build(ll k,ll l, ll r)
{
    
    
	t[k].l=l;t[k].r=r;
	if(l==r)
	{
    
    
		t[k].w=a[l];
		t[k].lzy=0;
		return;
	}
	ll mid=l+r>>1;
	build(ls,l,mid);
	build(rs,mid+1,r);
	pushup(k);
}
inline void pushdown(ll k)
{
    
    
	t[ls].w+=t[k].lzy*(t[ls].r-t[ls].l+1);
	t[rs].w+=t[k].lzy*(t[rs].r-t[rs].l+1);
	t[ls].lzy+=t[k].lzy;
	t[rs].lzy+=t[k].lzy;
	t[k].lzy=0;
}
void modify(ll k, ll x, ll y, ll v)
{
    
    
	if(x<=t[k].l&&t[k].r<=y)
	{
    
    
		t[k].w+=v*(t[k].r-t[k].l+1);
		t[k].lzy+=v;
		return ;
	}
	pushdown(k);
	ll mid=(t[k].l+t[k].r)>>1;
	if(x<=mid)modify(ls,x,y,v);
	if(y>=mid+1)modify(rs,x,y,v);
	pushup(k);
}
ll query(ll k, ll x, ll y)
{
    
    
	if(x<=t[k].l&&t[k].r<=y)return t[k].w;
	pushdown(k);
	ll mid=t[k].l+t[k].r>>1,ans=0;
	if(x<=mid)ans+=query(ls,x,y);
	if(y>mid)ans+=query(rs,x,y);
	return ans;
}
int main()
{
    
    
	read(n);read(m);
	for(int i=1;i<=n;i++)read(a[i]);
	build(1,1,n);
	for(int i=1;i<=m;i++)
	{
    
    
		read(s);
		if(s==1)
		{
    
    
			read(p);read(q);read(r);
			modify(1,p,q,r);
		}
		if(s==2)
		{
    
    
			read(p);read(q);
			printf("%lld\n",query(1,p,q));
		}
	}
}

这里推荐几道题

  1. luoguP3373 【模板】线段树 2
  2. FZOJ P2457: [Usaco2008 Feb]Hotel 旅馆

总结

线段树是 O I OI OI中用途广泛也非常重要的一个数据结构 学好是必要的 有困难可以留在评论区大家一起讨论 或者加我的QQ407694747一起讨论 毕竟我学的也不好233
有问题请各路神犇指正 今天就说到这吧 有时间我会把剩下两篇的题解做出来的!

猜你喜欢

转载自blog.csdn.net/dhdhdhx/article/details/95605011