线段树详解

引入

有这样一类问题 设有长度为n的数列{a1,a2,a3…an} 需要进行一些操作 比如求区间的最大值 (求某个区间的和 或者在进行某些修改操作后再求区间的和 ) 如果用普通的数组存储数列 然后进行暴力求解的话总复杂度是很大的(当数列很长 查询的次数很多时) 这种方法肯定是不可取的
对于这种问题,有一种神奇的数据结构,能在O(mlog2 n)的时间内解决,这就是线段树。

线段树的概念

线段树是一种用于区间处理的数据结构,用二叉树来构造。线段树是建立在线段(或者区间)基础上的树,树的的每个节点代表一条线段[L,R].
在这里插入图片描述
这就是线段树的结构,线段树是二叉树,一个区间每次被折一半往下分,所以最多分log2 n次就能到达最底层。当需要查找一个点或者区间的时候,顺着节点往下找,最多log2 n次就能找到。这就是线段树效率高的原因,它使用了二叉树折半查找的方法。
如果需要修改元素,直接修改叶子节点上元素的值后从底往上更新线段树,操作次数也是O(log2 n);

线段树的构造

那你学习线段树不得知道怎么构建一个线段树吗?
差不多就是这样 如下图
在这里插入图片描述
(画的有点丑)
将这个线段树整合放到数组里面就是下面这样的tree[]数组 打×的位置是没有此节点的 线段树节点旁边的数字就是在数组中的下标
很容易发现 当前a节点(node为a节点的下标)的左孩子的下标left=node2+1,
右孩子的下标right=node
2+2;
现在我们就有两个数组了一个原始数组arr[],一个线段树数组tree[];

void build(ll l, ll r, ll node) {//构建一棵树 node为线段树的节点
	if (l == r) {
		tree[nod] = arr[r];
		return;
	}
	int mid = (l + r) / 2; 
	build(l, mid, node * 2);
	build(mid + 1, r, node * 2 + 1);
	tree[node] = tree[node * 2] + tree[node * 2 + 1];
}

其实就是运用递归 折半进行构造的

点修改

在学习线段树的途中 要搞清楚如何进行点修改?
arr[10]={1,3,5,7,9,11};
那如果现在你要对arr[]数组中的arr[2] 加一个2,在线段树中是如何实现的呢?
我们先要查询到这个arr[2]在线段树处于哪个位置 所以我们要从线段树的0号节点开始 往下折半
可以看到 arr[2] 处在[0-2]的区间中 然后继续折半 找到了arr[2]在线段树的4号节点上 所以直接对这个节点进行修改操作 也就是 tree[4] +=2, 但是tree[4]的父节点tree[1]中的值储存的是子节点tree[3]和tree[4]的值 子节点变了 那父节点也要跟着变 所以tree[1]=tree[3]+tree[4] 那tree[1]变了 tree[1]的父节点也要跟着变啊 以此类推 往上进行更新
代码实现如下

void update(ll s, ll t, ll l, ll r, ll node) {//单点修改 arr[s]+=t; l,r为arr[]的左右边界
	if(s<l||s>r) return;
	if (l == r&&r==s) { 
		tree[node] += t;
	}
	else {
		int mid = (l + r) / 2;
		update(s, t, l, mid, nod * 2);
		update(s, t, mid + 1, r, nod * 2 + 1);
		tree[nod] = tree[nod * 2] + tree[nod * 2 + 1];
	}
}

线段树的求和

比如现在你就要求arr[]数组的arr[0]到arr[4]中所有数的和
一般操作来说 我们需要找到arr[]数组中的值在tree[]数组中对应的值然后再加起来
但是既然我们已经把线段树给构造出来了 肯定是不会用这个很费时间的方法了
同理 从tree[]的根节点开始查找 然后开始往下查询 但是呢当你查询到一个区间在你所要求和的区间之内的话 就可以直接返回tree[]这个区间的值 而不用再往下查询了
例如sum[0-4]=arr[0]+arr[1]+arr[2]+arr[3]+arr[4]=tree[0-2]+tree[3-4];

ll query(ll L, ll R, ll l, ll r, ll nod) {//求和操作 
	ll res = 0;
	if (L <= l && R >= r) return tree[nod];
	int mid = (l + r) / 2;
	if (L <= mid) res += query(L, R, l, mid, nod * 2);//求左区间
	if(R>mid) res+=query(L, R, mid + 1, r, nod * 2 + 1);//右区间
	return res;
}

线段树的区间修改

点修改只能修改线段树上的某个点。区间修改是很复杂的问题。
这里就要引入一种“懒惰(lazy)”的做法。当修改的是一个整块区间时,只对这个线段区间进行整体上的修改,其内部每个元素的内容先不做修改,只有当这部分线段的一致性被破坏时才把变化值传递给子区间,那么,每次区间修改的复杂度是O(log2 n),总复杂度是O(nlog2 n)。 对于lazy操作的实现,需要对每次操作的子区间记录状态;
需要定义一个lazy[]数组储存修改
因为这个lazy数组很懒 所以只有用到它时才会往下更新

void pushdown(ll nod, ll l, ll r) {//向下更新 
	if (lazy[nod]) {
		lazy[nod * 2] += lazy[nod];//左孩子继承父节点的更新
		lazy[nod * 2 + 1] += lazy[nod];//右孩子继承父节点的更新
		int mid = (l + r) / 2;
		tree[nod * 2] += lazy[nod] * (mid - l + 1);//同理
		tree[nod * 2 + 1] += lazy[nod] * (r - mid);
		lazy[nod] = 0;
	}
}
void update(ll L,ll R,ll val,ll l,ll r,ll nod) {//区间修改操作 在区间L到R之间都加上一个val 
	if (R < l || r < L) return;
	if (L <= l && r <= R) {
		tree[nod] += (r - l + 1) * val;
		lazy[nod] += val;
	}
	else {
		pushdown(nod, l, r); //开始继承操作
		int mid = (l + r) / 2;
		update(L, R, val, l, mid, nod * 2);
		update(L, R, val, mid + 1, r, nod * 2 + 1);
		tree[nod] = tree[nod * 2] + tree[nod * 2 + 1];
	}
}

ll query(ll L, ll R, ll l, ll r, ll nod) {//求和操作
	ll res = 0;
	if (L <= l && R >= r) return tree[nod];
	pushdown(nod, l, r);//要用到这个lazy[]数组了 开始继承
	int mid = (l + r) / 2;
	if (L <= mid) res += query(L, R, l, mid, nod * 2);
	if(R>mid) res+=query(L, R, mid + 1, r, nod * 2 + 1);
	return res;
}

但是要注意lazy[]数组里面储存的是变化的值 而tree[]是实际变化的 所以当区间被修改的时候
区间的和(也就是线段树的节点也要被修改)
与单点修改不同的是 引入了lazy[]数组记录区间修改的值
当然这里的修改只涉及到加减操作 如果涉及到乘除的话(以后再补充吧😂)

说了这么多举个例子看看吧
P3372 【模板】线段树 1
其实这就是一道模板题
只要你熟悉了线段树 知道了lazy[]的使用 直接码就好了
注意数据的范围有点大 要用long long;
给个模板吧

#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
typedef long long ll;
const int maxt = 100100;
ll h[maxt], tree[maxt * 4], lazy[maxt * 4];
void pushdown(ll nod, ll l, ll r) {//向下更新 因为这个lazy数组很懒 所以只有用到它是才会往下更新
	if (lazy[nod]) {
		lazy[nod * 2] += lazy[nod];
		lazy[nod * 2 + 1] += lazy[nod];
		int mid = (l + r) / 2;
		tree[nod * 2] += lazy[nod] * (mid - l + 1);
		tree[nod * 2 + 1] += lazy[nod] * (r - mid);
		lazy[nod] = 0;
	}
}

void build(ll l, ll r, ll nod) {//建树
	if (l == r) {
		tree[nod] = h[r];
		return;
	}
	int mid = (l + r) / 2;
	build(l, mid, nod * 2);
	build(mid + 1, r, nod * 2 + 1);
	tree[nod] = tree[nod * 2] + tree[nod * 2 + 1];
	    
}
void update1(ll s, ll t, ll l, ll r, ll nod) {//单点修改 h[s]+=t;
	if (l == r) {
		tree[nod] += t;
	}
	else {
		int mid = (l + r) / 2;
		update(s, t, l, mid, nod * 2);
		update(s, t, mid + 1, r, nod * 2 + 1);
		tree[nod] = tree[nod * 2] + tree[nod * 2 + 1];
	}
}

void update(ll L,ll R,ll val,ll l,ll r,ll nod) {//区间修改操作 在区间L到R之间都加上一个val 
	if (R < l || r < L) return;
	if (L <= l && r <= R) {
		tree[nod] += (r - l + 1) * val;
		lazy[nod] += val;
	}
	else {
		pushdown(nod, l, r);
		int mid = (l + r) / 2;
		update(L, R, val, l, mid, nod * 2);
		update(L, R, val, mid + 1, r, nod * 2 + 1);
		tree[nod] = tree[nod * 2] + tree[nod * 2 + 1];
	}
}

ll query(ll L, ll R, ll l, ll r, ll nod) {//求和操作
	ll res = 0;
	if (L <= l && R >= r) return tree[nod];
	pushdown(nod, l, r);
	int mid = (l + r) / 2;
	if (L <= mid) res += query(L, R, l, mid, nod * 2);
	if(R>mid) res+=query(L, R, mid + 1, r, nod * 2 + 1);
	return res;
}

int main()
{
	int n, m;
	cin >> n >> m;
	for (int i = 1; i <= n; i++)
		cin >> h[i];
	build(1, n, 1);
	int a, b, c, d;
	while (m--) {
		cin >> a;
		ll res = 0;
		if (a == 1) {
			cin >> b >> c >> d;
			update(b, c, d, 1, n, 1);
		}
		else {
			cin >> b >> c;
			res = query(b, c, 1, n, 1);
			printf("%lld\n", res);
		}
	}
	return 0;
}

本人小白 如有不正确的地方 欢迎指正
//南昌理工ACM集训队

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值