浅谈 线段树

命题描述

给定数列 a[1], a[2]…a[n] ,你需要依次进行 q 个操作,操作有两类:

1 i x:给定 i,将 a[i] 加上 x;
2 l r:给定 l r,求 a[l] + a[l + 1] + a[l + 2] + … + a[r - 1] + a[r] 的值

  • 输入格式
    第一行 包含 2 个正整数 n,q,n 表示数列长度,q 表示询问个数。
    第二行 n 个整数 ,表示初始数列.
    接下来 q 行,每行一个操作,为以下两种之一:

    1 i x:给定 i,x,将 a[i] 加上 x;
    2 l r:给定 l, r,求 a[l] + a[l + 1] + a[l + 2] + … + a[r - 1] + a[r] 的值。

  • 输出格式
    对于每个 2 l r 操作输出一行,每行有一个整数,表示所求的结果。

样例输入
3 2
1 2 3
1 2 0
2 1 3
样例输出
6
分析

首先你会发现,这个命题和树状数组的单点修改,区间查询没有一点区别。

但我们今天不用树状数组来解绝这个问题,而是使用另一种树形结构:线段树


定义

线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。

使用线段树可以快速的查找某一个节点在若干条线段中出现的次数,时间复杂度为 O ( l o g N ) O(logN) O(logN)。而未优化的空间复杂度为 2 N 2N 2N,实际应用时一般还要开 4 N 4N 4N 的数组以免越界,因此有时需要离散化让空间压缩。


step 1. 建树

我们定义一棵线段树,它的结点为 i i i,左儿子为 i ∗ 2 i * 2 i2, 右儿子为 i ∗ 2 + 1 i * 2 + 1 i2+1

每一个结点存储一个区间,以及这个区间的和,且每个结点存储的区间应该是它的两个儿子存储的区间合起来。(所以为了方便实现,我们可以在每个结点中再存储一个 m i d mid mid,表示它的两个子结点存储的区间是从哪里分开的

假设一个总长度为 5 5 5 的序列,以它为原序列所建的线段树如下:
在这里插入图片描述
在实现这方面,我们采用递归建树,具体实现:

struct tree { // 定义一棵树
	long long x; // x表示它存储的区间的和
	int l, r, mid; 
	// l表示区间左端点,r表示区间右端点,mid表示自己的子结点是在哪里分开的
} t[MAXM];
void Make_Tree(int i, int l, int r) {
	// i号结点,表示的区间是[l, r] 
	t[i].l = l; 
	t[i].r = r;
	// 在i号中存储[l,r]区间左右端点的信息
	if(l == r) t[i].x = a[l]; 
	// 如果左右端点相同,则这个结点在树中处于叶结点的位置
	// 其区间和就是
	else {
        int mid = (r + l) >> 1;
        // 我们定义儿子结点一定是从父亲结点存储的区间的中间分开
        t[i].mid = mid;
        Make_Tree(i * 2, l, mid); // 递归左儿子建树
        Make_Tree(i * 2 + 1, mid + 1, r); // 递归右儿子建树
        t[i].x = t[i * 2].x + t[i * 2 + 1].x;
        // 根据定义,父结点存储的区间和是两个儿子存储的区间和的和
	}
}
step 2. 修改

首先每个结点是保存的区间和对吧。

所以修改思路很简单,将每一个包含了要修改的点的区间对应的结点保存的区间和都加一下,即可。

依然使用递归思路,我们直接来看看代码:

void Update(int i, int k, long long add) { 
	// i号结点,k表示要更改的数的位置,add表示要加多少
	t[i].x += add; // 能进入这个函数,就表示i号结点的区间中一定包含k号元素,所以可以直接将i号结点存储的区间和更新
	if(t[i].l < t[i].r) { // 如果还没到叶结点,即还有儿子的话
		if(k <= t[i].mid)
		// 如果左儿子存储的区间包含元素k
			Update(i * 2, k, add); // 递归更改
		else 
		// 如果右儿子存储的区间包含元素k
			Update(i * 2 + 1, k, add); // 递归更改
	}
	return ;
}
step 3. 查询

每次是查询一个区间嘛,树状数组是维护的前缀和,而线段树可以直接求出某个区间的和。

首先,每个要查询区间一定是包含在结点 1 1 1 存储的区间里的,所以我们可以直接从结点 1 1 1 开始往下找。对于每一个结点我们都都可以确定要查询的区间是在它的左儿子还是右儿子甚至两个儿子各有一丢丢,然后向那个儿子递归查询即可

上代码:

long long Find(int i, int l, int r) {
	// i号结点,l,r表示当前需要查询的区间的左右端点
	if(t[i].l == l && t[i].r == r) // 如果当前结点存储的就是我们想要的区间?直接返回
		return t[i].x;
	if(r <= t[i].mid) // 如果要查询的区间在当前结点的左儿子里
		return Find(i * 2, l, r); // 递归访问左儿子
	else if(l > t[i].mid) // 如果要查询的区间在当前结点的右儿子里
		return Find(i * 2 + 1, l, r); // 递归访问右儿子
	else // 如果各有一丢丢?
		return Find(i * 2, l, t[i].mid) + Find(i * 2 + 1, t[i].mid + 1, r); 
		// 分别递归访问两个儿子,并把访问后得到的结果相加
}
part 4. 总体实现:
#include <cstdio>
#include <algorithm>
using namespace std;

const int MAXN = 500500;
const int MAXM = 1050000;
struct tree {
	long long x;
	int l, r, mid;
} t[MAXM];
int a[MAXN];
int n, m;

void Make_Tree(int i, int l, int r) {
	t[i].l = l;
	t[i].r = r;
	if(l == r) t[i].x = a[l];
	else {
        int mid = (r + l) >> 1;
        t[i].mid = mid;
        Make_Tree(i * 2, l, mid);
        Make_Tree(i * 2 + 1, mid + 1, r);
        t[i].x = t[i * 2].x + t[i * 2 + 1].x;
	}
}

void Update(int i, int k, long long add) {
	t[i].x += add;
	if(t[i].l < t[i].r) {
		if(k <= t[i].mid)
			Update(i * 2, k, add);
		else 
			Update(i * 2 + 1, k, add);
	}
	return ;
}

long long Find(int i, int l, int r) {
	if(t[i].l == l && t[i].r == r) 
		return t[i].x;
	if(r <= t[i].mid) 	
		return Find(i * 2, l, r);
	else if(l > t[i].mid)
		return Find(i * 2 + 1, l, r);
	else 
		return Find(i * 2, l, t[i].mid) + Find(i * 2 + 1, t[i].mid + 1, r); 
}

int main() {
	int n, m;
	scanf ("%d %d", &n, &m);
	for(int i = 1; i <= n; i++) scanf ("%d", &a[i]);
	Make_Tree(1, 1, n);
	for(int i = 1; i <= m; i++) {
		int flag;
		scanf ("%d", &flag);
		if(flag == 1) {
			int x, k;
			scanf ("%d %d", &x, &k);
			Update(1, x, k);
		}
		else {
			int l, r;
			scanf ("%d %d", &l, &r);
			printf("%d\n", Find(1, l, r));
		}
	}
	return 0;
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值