1. 线段树基础:从入门到精通
线段树(Segment Tree)是算法竞赛中最经典的数据结构之一,它能在O(logn)时间内完成区间查询和区间更新操作。我第一次接触线段树是在解决一道区间最大值问题时,当时就被它优雅的二分思想所折服。
线段树的本质是一棵完全二叉树,每个节点代表一个区间。以区间求和为例,根节点存储整个区间的和,其左右子节点分别存储左半区间和右半区间的和,如此递归下去直到叶子节点存储单个元素的值。这种结构使得我们能够快速合并子区间的信息。
1.1 线段树的实现要点
构建线段树时,我习惯使用数组而非指针来实现,这样既节省空间又便于编码。假设原始数组有n个元素,我们通常需要开4n大小的数组来存储线段树:
cpp复制const int MAXN = 1e5 + 5;
int arr[MAXN]; // 原始数组
int tree[4*MAXN]; // 线段树数组
void build(int node, int start, int end) {
if (start == end) {
tree[node] = arr[start];
return;
}
int mid = (start + end) / 2;
build(2*node, start, mid);
build(2*node+1, mid+1, end);
tree[node] = tree[2*node] + tree[2*node+1]; // 合并子节点信息
}
注意:虽然理论上线段树只需要2n-1的空间,但在递归实现时,最坏情况下需要4n的空间才能保证不越界。这是我曾经踩过的坑,特别是在处理n不是2的幂次的情况时。
1.2 区间查询的优化技巧
区间查询是线段树的核心操作。假设我们要查询区间[L, R]的和,算法会从根节点开始,递归地检查当前节点代表的区间是否完全包含在[L, R]中:
cpp复制int query(int node, int start, int end, int L, int R) {
if (R < start || L > end) return 0; // 完全不重叠
if (L <= start && end <= R) return tree[node]; // 完全包含
int mid = (start + end) / 2;
int left = query(2*node, start, mid, L, R);
int right = query(2*node+1, mid+1, end, L, R);
return left + right;
}
在实际比赛中,我发现一个常见的优化是添加"剪枝"条件:当当前节点的值为0(或其他特定值)时可以直接返回,这在某些特殊问题中可以显著提升效率。
