线段树
是一个二叉树,用于求出数组中一段数的和(前缀和)、给某个位置的数加上一个数、求一段区间的最大值
支持的操作:单点修改、区间查询
时间复杂度:修改和查询都是 O ( l o g n ) O(logn) O(logn)
线段树的核心操作
pushup(u):用子节点的信息来更新当前节点信息
build(u, l, r):在一段区间上初始化线段树, u表示根结点,l表示左边界,r表示右边界
modify(u, l, r):单点修改, u当前节点的编号,x要修改的位置,v要修改的值
query(u, x, v):查询一段区间的和,u表示根结点,l表示左边界,r表示右边界
线段树的储存结构
使用一维数组存节点
x的父节点编号=x/2
x的左儿子编号=2x
x的右儿子编号=2x+1
一段区间[1,2,3,4,5,6,7]的线段树表示
每一段区间里面存的都是它两个子节点的数的和
单点修改操作
区间查询查询
从区间[1,7]往下递归,查询a[2]~a[5]的和
题目
给定 n个数组成的一个数列,规定有两种操作,一是修改某个元素,二是求子数列 [a,b] 的连续和。
输入格式
第一行包含两个整数 n 和 m,分别表示数的个数和操作次数。
第二行包含 n 个整数,表示完整数列。
接下来 m 行,每行包含三个整数 k,a,b (k=0,表示求子数列[a,b]的和;k=1,表示第 a个数加 b)。
数列从 1 开始计数。
输出格式
输出若干行数字,表示 k=0时,对应的子数列 [a,b]的连续和。
输入样例:
10 5
1 2 3 4 5 6 7 8 9 10
1 1 5
0 1 3
0 4 8
1 7 5
0 4 8
输出样例:
11
30
35
代码
#include <iostream>
#include <algorithm>
#include <cstring>
#include <vector>
using namespace std;
const int N = 100010;
int n, m;
int w[N];//记录一下权重
struct Node
{
// 左右区间
int l, r;
// 当前区间的sum=左右两个儿子的sum之和
int sum;
}tr[N * 4];// 开 4 倍空间,防止溢出,用一维数组储存每个结点
// 用2个子节点的信息来更新当前节点信息
void pushup(int u)
{
// 当前节点的sum=左右儿子的sum之和 u << 1 | 1 =2x+1
tr[u].sum = tr[u << 1].sum + tr[u << 1 | 1].sum;
}
// 在一段区间上初始化线段树
// 当前节点编号,左右边界
void build(int u, int l, int r)
{
// 是叶结点
// 叶结点的sum赋值给它自己
if (l == r)
{
tr[u] = { l,r,w[r] };
}
else
{
//否则的话,说明当前区间长度至少是 2 ,那么我们需要把当前区间分为左右两个区间,那先要找边界点
// 赋值左右边界的初值
tr[u] = { l, r };
int mid = (l + r) >> 1;
// 递归左右儿子
build(u << 1, l, mid);
build(u << 1 | 1, mid + 1, r);
// 修改完子节点之后,用子节点的信息来更新当前节点信息
pushup(u);
}
}
// 查询一段区间的和 u根节点编号,l r查询的区间
// 查询的过程是从根结点开始往下找对应的一个区间
int query(int u, int l, int r)
{
// 当前区间tr[u].l~tr[u].r已经被查询区间l~r完全包含,直接返回它的值
if (l <= tr[u].l && tr[u].r <= r)
{
return tr[u].sum;
}
//否则的话我们需要去递归来算
//计算当前 区间的中点是多少
int mid = (tr[u].l + tr[u].r) >> 1;
int sum = 0;//sum 来表示一下我们的总和
// 判断当前区间的中点和查询区间的左边有没有交集
if (l <= mid)
{
// 到下一层的左边区间查找
sum += query(u << 1, l, r);
}
// 判断当前区间的中点和右边有没有交集
if (r > mid)
{
// 和右边有交集 右区间的左端点=mid+1
// 到下一层的右边区间查找
sum += query(u << 1 | 1, l, r);
}
return sum;
}
// 单点修改
// u当前节点的编号,x要修改的位置,v要修改的值
void modify(int u, int x, int v)
{
// 如果当前已经是叶节点了,那我们就直接让他的总和加上 v 就可以了
if (tr[u].l == tr[u].r)
{
tr[u].sum += v;
}
else
{
// 当前已经不是叶节点
int mid = (tr[u].l + tr[u].r) >> 1;
// 判断x在左半边还是右半边
if (x <= mid)
{
// 在左半边,那就修改左儿子
modify(u << 1, x, v);
}
else
{
// 在右半边,那就修改右儿子
modify(u << 1 | 1, x, v);
}
// 更新完之后,用子节点的信息来更新当前节点信息
pushup(u);
}
}
int main()
{
cin >> n >> m;
for (int i = 1; i <= n; i++)
{
cin >> w[i];
}
/*根节点的下标=1,初始区间是 1 到 n */
build(1, 1, n);
int k, a, b;
while (m--)
{
cin >> k >> a >> b;
if (k == 0)
{
// 求和,根节点编号,查询的区间
cout << query(1, a, b) << endl;
}
else
{
// 根节点的下标,要修改的位置,是要修改的值
modify(1, a, b);
}
}
return 0;
}