线段树详解(超详细)

前言

这篇文章,应该是我写的到目前为止最难也应该是最长的,(也不能说最难吧,反正也不知道怎么说)的一篇文章。

其实线段树吧,你之前可能听说过,是一种很难的算法。但是学过之后,又觉得不是那么难(反正我是这样的)。

为了保证你的阅读体验,请先学习:

  • 树(数据结构)(必修)
  • 位运算(必修)
  • 树状数组(选修)

好的那么废话不多说我们开始吧。

为什么要有线段树

假设有一个数列{1,1,2,3,4,6}

需要实现以下两点:

  1. 对一个区间进行求和操作
  2. 对一个区间进行增加操作

我们逐个来看。

区间求和

首先第一点很容易实现:

for(int i = 1; i <= n; i++){
	cin >> a[i];
}
for(int i = l; i <= r; i++){
	sum+=a[i];
}
cout << sum;

这是 O ( n ) O(n) O(n)的时间复杂度。

如果想要 O ( 1 ) O(1) O(1)的时间复杂度怎么办呢?

那就用前缀和:

for(int i = 1; i <= n; i++){
	cin >> a[i];
	f[i]=f[i-1]+a[i];
}
cout << f[l]-f[r-1];

这样就实现了 O ( 1 ) O(1) O(1)的时间复杂度。

区间增加

这个也很简单。

for(int i = 1; i <= n; i++){
	cin >> a[i];
}
for(int i = l; i <= r; i++){
	a[i]+=t;
}

如果想要做到 O ( 1 ) O(1) O(1)的时间复杂度,那就要用差分数组。

for(int i = 1; i <= n; i++){
	cin >> a[i];
	c[i]=a[i]-a[i-1];
}
c[l]+=t;
c[r+1]-=t;

一个问题

假如两个一起来呢?

那怎么办?还能用前缀和与差分吗?

但是前缀和和差分不能一起用。。。。。

如果暴力的话,时间复杂度是 O ( t n ) ( t 是操作次数 n 是数据量 ) O(tn)(t\texttt{\small是操作次数}n\texttt{\small是数据量}) O(tn)(t是操作次数n是数据量)

这个时候,线段树就出现了。

线段树是什么

顾名思义,就是把一个树存在线段里面

而这棵树的叶子节点就是数组里面的数据,两个节点的爸爸节点就是它们的和。

让我们来看一张生动形象但并不怎么好看的图片:

线段树
有点丑,不要在意。

先看白色部分:
可以看到最底层只有一个数字,代表原数组的下标,而其他的(l~r)代表的是下标l到下标r的和。

再看黑色部分:
可以看到,黑色的数是从左到右,从上到下按顺序标的,而且还有一个规律:一个数所在的节点的左儿子的数是它的两倍,右儿子是它的两倍加一,这就是树状数组的下标。

好的,我们引出了一个概念,树状数组。

就是把一个树存进一个数组里面,对于每一个下标 i i i,他的左儿子是下标 2 i 2i 2i,右儿子是下标 ( 2 i + 1 ) (2i+1) (2i+1)

那么现在又出来一个问题:

树状数组开多大

不想证明了,想要看证明的去这里,或者读以下片段(摘自前面的文章)

2 k ⩽ n ⩽ 2 k + 1 \large 2^{k}\leqslant n\leqslant 2^{k+1} 2kn2k+1,即 k ⩽ log ⁡ 2 n ⩽ k + 1 \large k\leqslant \log_{2}n\leqslant k+1 klog2nk+1时,线段数所开大小与叶子数为 2 ( k + 1 ) 2^(k+1) 2(k+1)的满二叉树大小相同,而该满二叉树的节点数 s i z e = 2 ( k + 2 ) − 1 size=2^{(k+2)}-1 size=2(k+2)1

2 k ⩽ n ⩽ 2 k + 1 \large 2^{k}\leqslant n\leqslant 2^{k+1} 2kn2k+1可知 ⌈ log ⁡ 2 n ⌉ = k + 1 \large \left \lceil \log_{2}n \right \rceil=k+1 log2n=k+1,则

2 k + 2 − 1 = 2 ⌈ log ⁡ 2 n ⌉ + 1 − 1 = 2 ⌈ log ⁡ 2 ( n ∗ 2 ) ⌉ − 1 \large 2^{k+2}-1=2^{\left \lceil \log_{2}n \right \rceil+1}-1=2^{\left \lceil \log_{2}(n*2) \right \rceil}-1 2k+21=2log2n+11=2log2(n2)1

2 k + 2 = 2 ⌈ l o g 2 ( n ∗ 2 ) ⌉ \large 2^{k+2}=2^{\left \lceil log_{2}(n*2) \right \rceil} 2k+2=2log2(n2)

因为 log ⁡ 2 ( n ∗ 2 ) ⩽ ⌈ log ⁡ 2 ( n ∗ 2 ) ⌉ < l o g 2 ( n ∗ 2 ) + 1 = l o g 2 ( n ∗ 4 ) \large \log_{2}(n*2)\leqslant \left \lceil \log_{2}(n*2) \right \rceil< log_{2}(n*2)+1=log_{2}(n*4) log2(n2)log2(n2)<log2(n2)+1=log2(n4)

所以 2 log ⁡ 2 ( n ∗ 2 ) ⩽ 2 ⌈ log ⁡ 2 ( n ∗ 2 ) ⌉ < 2 l o g 2 ( n ∗ 2 ) + 1 = 2 l o g 2 ( n ∗ 4 ) \large 2^{\log_{2}(n*2)}\leqslant 2^{\left \lceil \log_{2}(n*2) \right \rceil}< 2^{log_{2}(n*2)+1}=2^{log_{2}(n*4)} 2log2(n2)2log2(n2)<2log2(n2)+1=2log2(n4)

变化一下: 2 n ⩽ 2 k + 2 < 4 n \large 2n\leqslant 2^{k+2}< 4n 2n2k+2<4n

最终 2 n − 1 ⩽ 2 k + 2 − 1 < 4 n − 1 \large 2n-1\leqslant 2^{k+2}-1< 4n-1 2n12k+21<4n1,即 2 n − 1 ⩽ s i z e < 4 n − 1 \large 2n-1\leqslant size<4n-1 2n1size<4n1

如有侵权,会删除(打 LaTeX \LaTeX LATEX用了好久)

结论:开 4 n 4n 4n即可。

现在开始正文。

建树

首先定义以下变量:

const int N=1005;
int n;
int tree[N>>2];

然后利用递归的方式建树。

void push_up(int node){//用于更新一个节点
	tree[node]=tree[node>>1]+tree[(node>>1)+1]
}
void build(int node,int l,int r){
	if(l==r){
		cin >> tree[node];//已经到叶子节点了,注意看上面那张图,叶子节点的l和r是相等的(所以我只写了一个数)。
		return;//结束
	}
	int mid = l+r>>1;
	build(node*2,l,mid);//左子树递归
	build(node*2+1,mid+1,r);//右子树递归
	push_up(node);//回溯更新父节点
}

然后主函数的话就比较简单了。。。

int main()
{
	cin >> n;
	build(1,1,n);
}

单点修改

单点修改很简单,类似与二分查找。

//node是当前树节点,lr是查找范围,id是数组下标,k是要增加的数
void change(int node,int l,int r,int id,int k)
{
	if(l==r){
		tree[node]+=k;//更新叶子节点
	}
	int mid=l+r>>1;
	if(id<=mid){//如果这个节点小于mid
		change(node>>1,l,mid,id,k);//找左子树
	}
	else//否则找右子树
	{
		change((node>>1)+1,mid+1,r,id,k);
	}
	push_up(node);//回溯更新父节点
}

懒标记

设想一个问题,假如让你对一个数组操作10亿次,再查询一次,那时间消耗将是巨大的,所以我们不妨先将操作记录下来,到要使用的时候,再去操作,这就是懒标记。

当使用到某个节点的时候,如果发现懒标记存在,那么就应该更新该节点,以及以下的所有节点的数据,方便使用。

我们开一个懒标记的数组(下文简称懒数组)。

int lazy[N>>2];

懒数组是用来记录单个节点要增加多少的。
我们需要定义一个函数来传递懒标记。

void push_down(int node,int l,int r){
	if(lazy[node]){
		int mid = l+r>>1;
		lazy[node>>1] += lazy[node];
		lazy[(node>>1)+1] += lazy[node];
		//顺便更新子节点
		tree[node>>1] += (mid-(l-1))*lazy[node];
		tree[(node>>1)+1] += (r-mid)*lazy[node];
		lazy[node] = 0;//清空该节点的懒标记
	}
}
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值