最简单的线段树

这种线段树支持 \(O(logn)\) 的单点修改与区间查询


1. 基本的线段树建立

利用分治的思想,一个大的区间被分为两个区间,之后再分,直到分到每个点为止

可以用结构体存储线段树的每个节点,节点的最基础的属性就是区间的左右端点,表示了区间的范围

// 这里所有区间的个数严格小于 N * 4,因此开 N * 4 个节点
struct Node {
  int l, r;
  int sum;
}tr[N << 2];
int a[N];

void build(int p, int l, int r) {
  tr[p] = {l, r};
  if (l == r) {
    tr[p].s = a[l];
    return;
  }
  int mid = l + r >> 1;
  build(p << 1, l, mid);
  build(p << 1 | 1, mid + 1, r);
  pushup(p);
}

2. \(pushup\)

在线段树中,每次儿子节点改变后,若这会影响父节点的值,就会用儿子节点的值,更新父节点的值,即为 \(pushup\) 的过程

void pushup(int p) {
  tr[p].sum = tr[p << 1].sum + tr[p << 1 | 1].sum;
}

3.单点修改

想要进行单点修改,会先用函数递归的找到需要修改的节点,修改之后,再利用 \(pushup\),用修改好的儿子节点更新父节点,最后,线段树中包含这个点的所有区间都会被更新

void update(int p, int l, int r, int k) {
  if (l == tr[p].l && r == tr[p].r) {
    tr[p].sum += k;
    return;
  }
  int mid = l + r >> 1;
  if (x <= mid) update(p << 1, l, r, k);
  else update(p << 1 | 1, l, r, k);
  pushup(p);
}

4.区间查询

区间的查询实际上是将线段树上彼此相邻的节点拼起来讨论,这个拼起来的操作实际上和 \(pushup\) 是相同的,都是把小区间凑成大区间,那么有一种返回值为结构体的 \(query\) 函数可以得到整个的区间,这样可以将整个区间的属性一并输出,对于要求输出多种查询的问题,可以简化代码(具体在拓展中)

int query(int p, int l, int r) {
  if (l <= tr[p].l && tr[p].r <= r) {
    return tr[p].s;
  }
  int res = 0;
  int mid = l + r >> 1;
  if (l <= mid) res += query(p << 1, l, r);
  if (r > mid) res += query(p << 1 | 1, l, r);
  return res;
}

用法

  1. 单点修改 + 区间查询
  2. 区间修改 + 单点查询(与差分并用)

拓展

我们知道,在线段树上的操作,是对于区间的拆分与拼接,那么可以将结构体作为函数的返回值,传递更多的信息,以及提供一些复杂的操作

// pushup(int p) 的同名函数
Node pushup(Node L, Node R) {
  return {L.l, R.r, L.sum + R.sum};
}

void pushup(int p) {
  tr[p] = pushup(tr[p << 1], tr[p << 1 | 1]);
}

void build(...) {
  ...
}

void update(...) {
  ...
}

Node query(int p, int l, int r) {
  if (l <= tr[p].l && tr[p].r <= r) {
    return tr[p];
  }
  int mid = l + r >> 1;
  if (y <= mid) return query(p << 1, l, r);
  else if (x > mid) return query(p << 1 | 1, l, r);
  else return pushup(tr[p << 1], tr[p << 1 | 1]);
}

那么,这样的操作有什么意义呢,答:可以增加一些对线段树的理解,以及一些题目需要这样写来传递区间的多个属性

posted @ 2025-03-05 13:12  he_jie  阅读(24)  评论(0)    收藏  举报