Day5:线段树与树状数组
线段树:
一、线段树的用途:
举例:假设有一个数字序列,我们需要对其中的一段区间内的所有元素求和,同时,不同次求和操作之间可能会更改序列中某些元素的值。
一种容易想到的方法是利用循环遍历求和操作所涉及的区间,每个元素逐个相加。显然,这种做法的时间复杂度是O(N)。
但如果使用线段树,我们可以将求序列任意区间元素和以及修改某一元素值的时间复杂度由原本的O(N)、O(1)同时变为O(logN)。
事实上,满足“区间结合律”的区间操作或者可以通过转化满足“区间结合律”的区间操作都可以应用线段树进行处理。
至于什么是“区间结合律”,请读者自行百度(doge)
二、线段树的构建思路:
我们通常将线段树构建为满二叉树,并用一维数组存储。将单个元素存储在二叉树的叶子节点上。可以容易地推出,当给定的原始序列含有n个元素时,我们需要大小为4 * n的一维数组来存储线段树(尽管满二叉树中有些叶子节点并未被我们使用,我们仍然选择构建满二叉树。)
我们继续讨论上面的区间求和问题。程序读入给定序列后,我们使得二叉树的某个节点存储序列的某个片段中所有元素的和。假设这个节点存储区间[A, B]中所有元素的和,当[A, B]包含不止一个元素时,我们使用它的左子节点存储区间[A, B]的左半子区间(即区间[A, Mid],其中Mid=(A+B)/2)的所有元素和;同理,右子节点存储区间[Mid+1, B](Mid=(A+B)/2)的所有元素和。一旦我们成功构建出这样一棵完全二叉树,我们便完成了线段树的创建。
如图,假定程序读入的序列为{1,2,3,4,5,6,7,8}
我们这样构建线段树,使得所有叶子节点存储原始元素值。其他节点存储其两子节点的和。
为了描述方便,我们从根节点出发,依次对节点编号,从1开始(如图中红色数字所示)。在使用数组存储线段树时,我们使用节点编号作为数组下标。当取根节点编号为1时,对于x号节点,其左子节点编号为2x,右节点编号为2x+1。
三、实现代码:
下面给出上述例子中线段树的代码实现:
下面采用递归思想实现建树、单点修改、快速求区间结果这三个线段树的基本功能。
#include <stdio.h>
struct node_t { //将节点定义为结构体
int L, R; //L、R表示此节点所对应的序列区间为[L,R]
int val; //val存储节点的值
};
/*
使用数组arr[]中的序列建立线段树
树存储在tree[]中
node为根节点编号(作为数组下标使用)
整个序列的范围是arr[start] 至 arr[end]
*/
void build_tree(int arr[], struct node_t tree[], int node, int start, int end) {
tree[node].L = start; //记录当前节点对应的区间
tree[node].R = end;
if(start == end) { //递归到叶子节点则赋值后返回
tree[node].val = arr[start];
return;
}
int mid = (end - start) / 2 + start; //这样写防止溢出
int left_node = mid << 1;
int right_node = mid << 1 | 1;
build_tree(arr, tree, left_node, start, mid); //分别构建左右子树
build_tree(arr, tree, right_node, mid + 1, end);
tree[node].val = tree[left_node].val + tree[right_node].val; //根据区间结合律计算本节点的值
}
/*
将arr[idx]的值改为val
并更新存储在tree[]中的树
node为根节点编号(作为数组下标使用)
*/
void update_tree(int arr[], struct node_t tree[], int node, int idx, int val) {
int start = tree[node].L;
int end = tree[node].R;
if(start == end) { //递归到叶子结点结束
tree[node].val = val;
arr[idx] = val;
return;
}
int mid = (end - start) / 2 + start;
int left_node = mid << 1;
int right_node = mid << 1 | 1;
if(start <= idx && idx <= mid) //范围判断后选择对应子树递归
update_tree(arr, tree, left_node, idx, val);
else
update_tree(arr, tree, right_node, idx, val);
tree[node].val = tree[left_node].val + tree[right_node].val;
}
/*
利用tree[]中存储的树计算[L, R]区间的元素和
node为根节点编号(作为数组下标使用)
*/
int query_tree(struct node_t tree[], int node, int L, int R) {
int start = tree[node].L;
int end = tree[node].R;
if(R < start || L > end) return 0; //询问区间不在当前节点对应区间内, 返回0
if(L <= start && end <= R) return tree[node].val; //询问区间包含当前节点对应的区间, 返回节点值
int mid = (end - start) / 2 + start;
int left_node = mid << 1;
int right_node = mid << 1 | 1;
int left_val = query_tree(tree, left_node, L, R); //分别对左右子树递归
int right_val = query_tree(tree, right_node, L, R);
return left_val + right_val;
}
#define MAX 64
int data[MAX]; //存储原始数据
struct node_t tree_data[4 * MAX]; //存储树
int main() {
//调用示例, 序列长度n, 起始编号为1
int n;
scanf("%d", &n);
for(int i = 1; i <= n; ++i) scanf("%d", &data[i]); //读取序列
build_tree(data, tree_data, 1, 1, n); //建立线段树
int a, b;
scanf("%d %d", &a, &b);
printf("%d\n", query_tree(tree_data, 1, a, b)); //输出[a, b]的所有元素和
return 0;
}
四、区间修改中的“懒”思想:待补充
五、带权线段树:待补充