问题引入
假设有这样的问题:有n个数,m次操作,操作分为:修改某一个数或者查询一段区间的值
数据范围是(1 <= n, m<=1e9)。
这种题大家一看就知道打暴力,但是一看数据范围就知道只能得部分。
我们之前学过的前缀和算法可以解决区间求和的问题,并且时间复杂度是O(1),但如果涉及到修改操作,前缀和数组都需要重新计算,时间复杂度也是O(n).
那么有没有什么东西能兼顾两者呢?这就是我们要学习的线段树!把修改和查询的时间复杂度都降到O(logn)!!!
算法思想
先来看一下线段树是什么东西!!!
有以下数组(为方便计算,数组下标从1开始)
我们把它转换成线段树,是长这样的:
1)叶子结点(绿色)存的都是原数组元素的值
2)每个父结点是它的两个子节点的值的和
3)每个父结点记录它表示区间的范围,如上图的“1-2”表示1到2的区间
下面我们来看看线段树是如何降低操作复杂度的!
查询操作
例如我们需要查询2-5区间的和
使用递归的思想:
2~5的和
=2~3的和+4~5的和
=1+4+4~5的和
=1+4+6
=11
总之,就是沿着线段树的划分把区间分开,再加到一块就行啦!
修改操作
例如,我们要把结点2的值由3->5,线段树需要沿着黄色部分一个一个改,直到根结点:
不管是修改操作还是查询操作,时间复杂度都是O(logn)
下一步我们来看怎么实现线段树!
算法实现
首先我们需要将原始数组建立成一颗线段树,然后在树的基础上提供查询和修改的操作。
建树
观察上图,我们发现线段树是一棵近似完全二叉树,利用完全二叉树的性质,我们就可以直接用一个数组来存它。
代码如下:
#include <bits/stdc++.h>
using namespace std;
const int N = 1e4;
struct node {
int l, r, sum;
};
node tree[N * 4 + 10];
int a[N + 10];
void build(int x, int l, int r) {
tree[x] = {l, r};//也可以写成tree[x].l = l, tree[x].r = r;
//初始化每个节点的左右边界
printf("%d:%d %d\n", x, l, r);
if(l == r) {
tree[x].sum = a[l];//只有叶子节点是真正赋值的,其他节点都要进行pushup操作
return;
}
int mid = l + r >> 1;
//递归左右儿子节点
build(x << 1, l, mid);
build(x << 1 | 1, mid + 1, r);
//递归完成后,进行pushup操作
tree[x].sum = tree[x * 2].sum + tree[x * 2 + 1].sum;
}
int main() {
int n;
cin >> n;
for(int i = 1; i <= n; i++) {
cin >> a[i];
}
printf("运行结果如下:\n");
build(1, 1, n);
for(int i = 1; i <= n; i++) {
if(n * 2 <= pow(2, i) - 1) {
n = i;
break;
}
}
for(int i = 1; i <= pow(2, n) - 1; i++) {
printf("tree[%d].sum = %d\n", i, tree[i].sum);
}
printf("在完全二叉树中,0表示这个空间没有数,但是占空间\n");
}
运行效果如下:
还有修改和查询操作没有呈现,敬请期待!