线段树详解

线段树是一种二叉搜索树,用于区间查询和修改操作,支持O(logN)的时间复杂度。相对于树状数组,线段树功能更强大但常数较大。本文介绍了线段树的基本原理、单点修改和区间查询的实现,并引入了懒惰标记的概念,用于优化区间修改的效率。
摘要由CSDN通过智能技术生成

引入

例题:(洛谷 P3372 【模板】线段树 1

已知一个数列,你需要进行下面两种操作:
1.将某区间每一个数加上 k k k
2.求出某区间每一个数的和。

我们可以使用树状数组来解决这道题,然而这次我们要回归正解了!我们要使用线段树

线段树

线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。
使用线段树可以快速的查找某一个节点在若干条线段中出现的次数,时间复杂度为O(logN)。而未优化的空间复杂度为2N,实际应用时一般还要开4N的数组以免越界,因此有时需要离散化让空间压缩。——百度百科

实 际 应 用 时 一 般 还 要 开 4 N 的 数 组 以 免 越 界 ! 实际应用时一般还要开 4N 的数组以免越界! 4N!

4 N 的 数 组 ! 4N 的数组! 4N!

4 N 的 数 组 ! 4N 的数组! 4N!

(血淋淋的教训)

线段树能够支持满足结合律的运算的区间操作,查改的时间复杂度均为 O ( l o g n ) O(logn) O(logn),与树状数组相比,它的功能更强大,但是具有常数大,码量大的缺点。对于此,你需要记住以下三点:

  1. 树状数组能解决的问题,线段树一定能解决;
    线段树能解决的问题,树状数组不一定能解决。
  2. 在能用树状数组解决的情况下,尽量用树状数组解决;
    在对常数要求高的情况下,尽量用树状数组解决。
  3. 线段树的代码较为复杂,写挂了的话建议重写。

好,那我们进入正题。

在解决上述区间查改的问题之前,我们先来看一下这个问题

对于这道树状数组的模板题,我们可以使用较为简单的线段树来实现。

我们来看一下线段树的结构图:
请添加图片描述

我们将其填入数字:
请添加图片描述

其中每一个长条就是所谓的“线段”,可以发现,父节点(父线段)被平均分成两个子节点(子线段)。

线段树的原理是什么呢?我们来看上面的图,在实际操作中,上图的线段实际上是不存在的,那么存在的是什么呢?是线段内各数的和。

这样,我们可以发现,每个节点的值都等于其两个儿子的和,叶子节点的值为原数。

请添加图片描述
我们来模拟一下单点修改的过程:

我们要将第 3 3 3 个数加上 2 2 2,也就是将 4 4 4 2 2 2

首先,修改叶子节点:

请添加图片描述
依次向上累加,注意,在实际操作中只是将父节点更新为子节点之和(push_up),而并不需要更新线段上的数值,这里是为了便于理解。

最后更新完是这样的:
请添加图片描述
那查询呢?

假设我们要求 [ 2 , 4 ] [2,4] [2,4] 的和,这时我们要做相反的操作,即从根开始。

首先,在根节点找到需操作的区间:

请添加图片描述
向下分割:

请添加图片描述
继续分割,直到分割到完整的线段,返回线段的值:

请添加图片描述
请添加图片描述

至此,我们已经解决了单点修改和区间求和。代码如下:

#include<iostream>
#include<cstdio>
#define MAXN 500010
using namespace std;
int vis[MAXN*4];
int n,m;
int op,x,y;
int a[MAXN];
void push_up(int p)//把子节点传递给父节点
{
	vis[p]=vis[p<<1]+vis[p<<1|1];
}
void build(int p,int l,int r)
{
	if(l==r)//叶子节点直接赋值
	{
		vis[p]=a[l];
		return;
	}
	int mid=(l+r)>>1;
	build(p<<1,l,mid);//左子树
	build(p<<1|1,mid+1,r);//右子树
	push_up(p);//上传
}
void update(int p,int l,int r,int x,int k)//修改
{
	if(l==r)
	{
		vis[p]+=k;
		return;
	}
	int mid=(l+r)>>1;
	if(x<=mid)
		update(p<<1,l,mid,x,k);//左
	if(x>=mid+1)
		update(p<<1|1,mid+1,r,x,k);//右
	push_up(p);
}
int query(int p,int l,int r,int L,int R)
{
	if(L<=l&&R>=r)
		return vis[p];
	int mid=(l+r)>>1;
	int res=0;
	if(L<=mid)
		res+=query(p<<1,l,mid,L,R);
	if(R>=mid+1)
		res+=query(p<<1|1,mid+1,r,L,R);
	return res;
}
int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)
		scanf("%d",&a[i]);
	build(1,1,n);
	for(int i=1;i<=m;i++)
	{
		scanf("%d%d%d",&op,&x,&y);
		if(op==1)
			update(1,1,n,x,y);
		else
			printf("%d\n",query(1,1,n,x,y));
	}
	return 0;
}

现在我们考虑一下区间查改的问题,显然,进行多次单点修改是不可行的,所以我们考虑更优的做法。

既然多次的单点修改是不行的,我们就要考虑直接的区间修改,然而可以发现,这样做的时间复杂度仍然不容乐观,因为它仍然需要依次修改所有的线段的值。

这时,我们就要采用这样一种思路:在查询时进行修改。查询是从根节点依次向下查询,所以我们可以给修改的线段打上标记,在查询时进行向下的传递(push_down)。这个标记我们给它一个形象的名字——懒标记

有一个人 x x x,他有两个儿子 a a a b b b,到了过年的时候,亲戚们把要送给 a a a b b b 的红包交给了 x x x x x x 家里急需用钱,于是 x x x 私吞了这两个红包。
“一个 100 100 100 元,又一个 100 100 100 元,我赚了 2 × 100 = 200 元 2\times100=200元 2×100=200
“爸,钱呢”
“我先给你写个欠条,欠你 100 100 100 元,欠你 100 100 100 元,写好了”
“那我们用钱的时候怎么办”
“用钱就拿着欠条跟我要,要了钱得把欠条给我”

所谓“懒标记”,就是“欠条”。

这样,我们可以在查询时进行值的传递,而无需耗费多余的时间。

代码

#include<iostream>
#include<cstdio>
#define MAXN 100010
struct tree
{
	int vis;
	int tag;
}
t[MAXN*4];
int n,m;
int op,x,y,k;
int a[MAXN];
void push_up(int p)
{
	t[p].vis=t[p<<1].vis+t[p<<1|1].vis;
}
void build(int p,int l,int r)
{
	t[p].tag=0;
	if(l==r)
	{
		t[p].vis=a[l];
		return;
	}
	int mid=(l+r)>>1;
	build(p<<1,l,mid);
	build(p<<1|1,mid+1,r);
	push_up(p);
}
void push_tag(int p,int l,int r,int k)//处理懒标记
{
	t[p].tag+=k;
	t[p].vis+=k*(r-l+1);
}
void push_down(int p,int l,int r)//将懒标记向下传递
{
	if(t[p].tag==0)
		return;
	int mid=(l+r)>>1;
	push_tag(p<<1,l,mid,t[p].tag);
	push_tag(p<<1|1,mid+1,r,t[p].tag);
	t[p].tag=0;
}
void update(int p,int l,int r,int L,int R,int k)
{
	if(L<=l&&R>=r)
	{
		push_tag(p,l,r,k);
		return;
	}
	push_down(p,l,r);
	int mid=(l+r)>>1;
	if(L<=mid)
		update(p<<1,l,mid,L,R,k);
	if(R>=mid+1)
		update(p<<1|1,mid+1,r,L,R,k);
	push_up(p);
}
int query(int p,int l,int r,int L,int R)
{
	if(L<=l&&R>=r)
		return t[p].vis;
	push_down(p,l,r);
	int mid=(l+r)>>1;
	int res=0;
	if(L<=mid)
		res+=query(p<<1,l,mid,L,R);
	if(R>=mid+1)
		res+=query(p<<1|1,mid+1,r,L,R);
	return res;
}
int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)
		scanf("%d",&a[i]);
	build(1,1,n);
	for(int i=1;i<=m;i++)
	{
		scanf("%d",&op);
		if(op==1)
		{
			scanf("%d%d%d",&x,&y,&k);
			update(1,1,n,x,y,k);
		}
		else
		{
			scanf("%d%d",&x,&y);
			printf("%d\n",query(1,1,n,x,y));
		}
	}
	return 0;
}//要过线段树1的话得开long long
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值