线段树是算法竞赛中常用的用来维护 区间信息 的数据结构。
线段树可以在O(log N)的时间复杂度内实现单点修改、区间修改、区间查询(区间求和,求区间最大值,求区间最小值)等操作。线段树维护的信息,需要满足可加性,即能以可以接受的速度合并信息和修改信息,包括在使用懒惰标记时,标记也要满足可加性(例如取模就不满足可加性,对4取模然后对3取模,两个操作就不能合并在一起做)。
定义结构体
struct node {
int l; // 线段树的左端点
int r; // 线段树的右端点
int value; // 线段上的值
};
建树
对于每个结点,应该有两个指针来表示该结点的左右孩子,用一个数组tree[]
来模拟一个树,那么,对于结点tree[v]
,它的左孩子结点为tree[v * 2]
,右孩子结点为tree[v * 2 + 1]
。在线段树中,左右孩子分别是父结点的子区间,如父结点代表段l ~ r
,令int mid = (l + r) / 2
;则左孩子表示区间l ~ mid
,右孩子表示区间为mid + 1 ~ r
。叶结点x表示区间是[x, x]
的一个值,即原数组a[]
中的一个元素。
void build(int v, int l, int r) // 对结点v进行建立,区间为l, r
{
tree[v].l = l;
tree[v].r = r;
if (l == r) {
// 进行结点的初始化
tree[v].value = a[r];
return;
}
int mid = (l + r) / 2;
build(v * 2, l, mid);
build(v * 2 + 1, mid + 1, r);
// 根据左右儿子更新当前结点
tree[v].value = tree[v * 2].value + tree[v * 2 + 1].value;
}
更新
在对某一段a[i] ~ a[j]
上的所有元素都加上一个值c的时候,以一种记录增量的方法,即给每个结点再加上一个域int add;
记录更新操作的增量c。初始时每个结点的add值均为0,当对2 ~ 5区间进行更新操作后,给该结点的add加上一个值c。再下次要对2 ~ 3结点进行更新或查询时,再将add传递到下面的孩子结点去。
void update(int v, int l, int r, int m) // 更新区间 l ~ r 加上数 m
{
if (tree[v].l == l && tree[v].r == r) { // 找到了, 更新并记录增量
tree[v].value += m * (r - l + 1);
tree[v].add = m;
return; // 记得 return 实际儿子没有更新, 省时
}
if (tree[v].add) { // 上面没走掉, 传递能量
tree[v * 2].add += tree[v].add; // 儿子本身可能也有增量
tree[v * 2 + 1].add += tree[v].add;
tree[v].add = 0; // 传递后, 清零增量
}
int mid = (tree[v].l + tree[v].r) / 2;
if (r <= mid) {
update(v * 2, l, r, m); // 只对左孩子进行更新
} else {
if (l > mid) {
update(v * 2 + 1, l, r, m); // 只对右孩子进行更新
} else { // 区间横跨了左右儿子区间,对其两者均进行更新
update(v * 2, l, mid, m);
update(v * 2 + 1, mid + 1, r, m);
}
}
}
查询
即查询区间 l ~ r 上的value值
void query(int v, int l, int r) // 当前查询结点为 v 要查询的区间是 l ~ r
{
if (tree[v].l == l && tree[v].r == r) {
ans += tree[v].value;
return;
}
if (tree[v].add) { // 上面没走掉, 传递能量
tree[v * 2].add += tree[v].add; // 儿子本身可能也有增量
tree[v * 2 + 1].add += tree[v].add;
tree[v].add = 0; // 传递后, 清零增量
}
int mid = (tree[v].l + tree[v].r) / 2;
if (r <= mid) {
query(v * 2, l, r); // 要查询的区间都在左儿子
} else {
if (l > mid) {
query(v * 2 + 1, l, r); // 要查询的区间都在右儿子
} else { // 要查询的区间分跨左右孩子
query(v * 2, l, mid);
query(v * 2 + 1, mid + 1, r);
}
}
}
线段树的用法非常灵活,离散化也是经常配合线段树使用的,因为线段树所需的空间是4 * MAX的,MAX为原数据个数。
上述内容为摘录书本,貌似有错误,以下示例代码
/// <summary>
/// 构建线段树
/// </summary>
/// <param name="v"></param>
/// <param name="l"></param>
/// <param name="r"></param>
public void BuildTree(int v, int l, int r) {
tree[v].l = l;
tree[v].r = r;
if (l == r) {
tree[v].value = red[r]; // 1 or 0
tree[v].add = 0;
return;
}
int mid = (l + r) / 2;
BuildTree(v * 2, l, mid);
BuildTree(v * 2 + 1, mid + 1, r);
tree[v].value = tree[v * 2].value + tree[v * 2 + 1].value;
}
// --------修改增量--------
// 1.向下搜索, 修改区间值, 修改增量值(子结点需要)
// 2.回溯更新父结点的区间值
// --------增量清零--------
// 清除增量前, a.修改子结点暂存的值 b.传递增量给子区间
/// <summary>
/// 更新线段树区间值
/// </summary>
/// <param name="v"></param>
/// <param name="l"></param>
/// <param name="r"></param>
/// <param name="add"></param>
public void UpdateTree(int v, int l, int r, int add) {
if (tree[v].l == l && tree[v].r == r) {
// 停止向下搜索, 更新该结点的增量值
tree[v].value += (r - l + 1) * add;
tree[v].add += add;
return;
}
int mid = (tree[v].l + tree[v].r) / 2;
if (tree[v].add != 0) { // 父结点有增量, 需要清除增量
tree[v * 2].value += (mid - tree[v].l + 1) * tree[v].add; // a.更新区间值
tree[v * 2 + 1].value += (tree[v].r - mid) * tree[v].add;
tree[v * 2].add += tree[v].add; // b.传递增量
tree[v * 2 + 1].add += tree[v].add;
tree[v].add = 0; // c.清除增量
}
if (r <= mid) { // 只更新左边
UpdateTree(v * 2, l, r, add);
} else if (mid < l) { // 只更新右边
UpdateTree(v * 2 + 1, l, r, add);
} else { // 二分, 更新左右边
UpdateTree(v * 2, l, mid, add);
UpdateTree(v * 2 + 1, mid + 1, r, add);
}
tree[v].value = tree[v * 2].value + tree[v * 2 + 1].value;
}
// --------查询区间--------
// 当前结点值 = 暂存的值 + 增加值 (区间长度 * (父结点)增量) // 须保证当前结点的祖先结点没有增量 !
// --------增量清零--------
// 清除增量前, a.修改子结点暂存的值 b.传递增量给子区间
/// <summary>
/// 查询线段树区间值
/// </summary>
/// <param name="v"></param>
/// <param name="l"></param>
/// <param name="r"></param>
private int QueryTree(int v, int l, int r) {
var tre = tree[v];
if (tree[v].l == l && tree[v].r == r) {
return tree[v].value;
}
int mid = (tree[v].l + tree[v].r) / 2;
if (tree[v].add != 0) { // 父结点有增量, 需要清除增量
tree[v * 2].value += (mid - tree[v].l + 1) * tree[v].add; // a.更新区间值
tree[v * 2 + 1].value += (tree[v].r - mid) * tree[v].add;
tree[v * 2].add += tree[v].add; // b.传递增量
tree[v * 2 + 1].add += tree[v].add;
tree[v].add = 0; // c.清除增量
}
if (r <= mid) { // 查询区域全在mid左边
return QueryTree(v * 2, l, r);
} else if (mid < l) { // 查询区域全在mid右边
return QueryTree(v * 2 + 1, l, r);
} else { // 查询区域在mid位于查询区域中间
return QueryTree(v * 2, l, mid) + QueryTree(v * 2 + 1, mid + 1, r);
}
}
public int QueryTree(int l, int r) {
if (l > r) return 0;
return QueryTree(1, l, r);
}
class node
{
public int add; // 增量
public int value; // 区间值
public int l, r; // 树上的左右端点
}