线段树学习笔记 I(建树/更新答案/区间查询)

线段树学习笔记

在这里插入图片描述

线段树的定义:一棵满二叉树。

假设一棵线段树中有 n n n 个节点,那么这一棵线段树存储需要使用 n + n 2 + n 4 + ⋯ + 2 + 1 ≈ 2 × n n + \frac{n}{2} + \frac{n}{4} + \cdots + 2 + 1 \approx 2\times n n+2n+4n++2+12×n 个节点。

线段树的每一个节点记录的都是所有需要求/询问的信息。

节点部分:

struct Node {
  int l, r;
  int ???, ...;
  void init(int p) { // 初始化
    l = r = p;
    ??? = ???;
    ......;
  }
} z[???];

假设现在要求 ∑ i = l r a i \sum_{i=l}^r a_i i=lrai 的值。

那么最底下的单点区间记录的都是这个节点的值。

第二行的区间假设为 [ 1 , 2 ] [1,2] [1,2],那么这个区间记录的和就是 a 1 + a 2 a_1 + a_2 a1+a2

考虑从下往上递归求解。

当左右儿子递归完成合并的时候,只需要将当前点的值更新为左儿子的值和右儿子的值的和即可。

如果求 max ⁡ i = l r a i \max_{i=l}^r a_i maxi=lrai 的值,那么把求和改成取最大值即可。

但是如果要求询问区间和,区间最小值和区间最大值,那么开三个数组比较麻烦,开一个结构体即可。

建树部分:

// 将左儿子和右儿子合并
Node operator + (const Node &lhs, const Node &rhs) {
  Node res;
  res.l = lhs.l;
  res.r = rhs.r;
  res.??? = ???;
  ......;
  return res;
}

// 其中 [l,r] 是当前维护的区间
// rt 是当前现在线段树节点的下标
void build(int l, int r, int rt) {
  if (l == r)
    // 最后一层节点 不能再分 更新当前节点
    z[rt].init(l);
    // init 成员函数是线性的
  else {
    int mid = l + r >> 1;
    // 规定左儿子对应区间为 l~mid 右儿子对应区间为 mid~r
    build(l, mid, rt << 1);
    build(mid + 1, r, rt << 1 | 1);
    z[rt] = z[rt << 1] + z[rt << 1 | 1];
    // 更新上一层的节点
    // 时间复杂度 O(1)
    // 需要重载运算符+
  }
}

线段树建树的整体复杂度为 O ( n ) O(n) O(n),因为每一个节点仅仅更新了一次。

假设现在只有一个求和操作:

struct Node {
  int l, r, sum;
  void init(int p) {
    l = r = p;
    sum = a[p];
  }
} z[N << 2];
Node operator + (const Node &lhs, const Node &rhs) {
  Node res;
  res.l = lhs.l;
  res.r = rhs.r;
  res.sum = lhs.sum + rhs.sum;
  return res;
}
void build(int l, int r, int rt) {
  if (l == r)
    z[rt].init(l);
  else {
    int mid = l + r >> 1;
    build(l, mid, rt << 1);
    build(mid + 1, r, rt << 1 | 1);
    z[rt] = z[rt << 1] + z[rt << 1 | 1];
  }
}

询问一段区间的和的时候,考虑将其拆分成一些段区间。

比如说头顶上的线段树,询问区间 [ 2 , 6 ] [2,6] [2,6] 的和,拆分出最小的段数,变为 [ 2 , 2 ] + [ 3 , 4 ] + [ 5 , 6 ] [2,2]+[3,4]+[5,6] [2,2]+[3,4]+[5,6] 这三个区间的和即可。

// 询问区间[nowl,nowr]的和
int query(int l, int r, int rt, int nowl, int nowr) {
  if (nowl <= l && r <= nowr)
    // 当前区间被完全包含 直接返回当前区间的和
    return z[rt].sum;
  int mid = l + r >> 1;
  if (nowr <= mid)
    // 右儿子没有 返回左儿子里递归出来的结果
    return query(l, mid, rt << 1, nowl, nowr);
  if (mid < nowl)
    // 同理
    return query(mid + 1, r, rt << 1 | 1, nowl, nowr);
  return query(l, mid, rt << 1, nowl, nowr) + query(mid + 1, r, rt << 1 | 1, nowl, nowr); // 左右儿子都得递归
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值