线段树, 算法竞赛掌管区间的神

线段树, 算法竞赛掌管区间的神

什么是线段树

上回讲树状数组的时候说过, 是一种分治数据结构, 把区间从中间劈开, 通过左子区间和右子区间的合并得到大区间.

上回的树状数组及其各种扩展.

线段树长什么样

观察线段树.

再次思考, 把区间从中间劈开, 通过左子区间和右子区间的合并得到大区间.

两个子区间的答案可以变成大区间的答案, 大区间又作为子区间去维护更大区间的答案.

线段树的存储

假设根结点为 \(1\), 按照BFS宽搜遍历树.

可以发现, \(x\) 的左儿子是 \(2x\), 右儿子是 \(2x + 1\).

然后又可以发现, 结点 \(1\)\(15\) 再加上 \(0\) 号 (虽然很少有人用), \(16\) 个结点恰好等于 \(2 * 8\) 也就是 \(2N\).

但是考虑 \(N \notin \{2^{x} | x \in \N\}\) (这个表达式是说 \(N\) 不是 \(2\) 的自然数幂), 下面会多一排, 而多的一排的长度是 \(2N\) (这是二叉树的性质), 所以线段树的空间是 \(2N + 2N = 4N\).

所以我们可以申请一个数组存储结点值.

ll val[NN << 2]; // NN << 2 = NN * 4

单点修改

首先, 不想讲建树是因为Defad不怎么喜欢建树, Defad喜欢 \(N\) 次单点修改当做建树, 都是 \(N\log{N}\), 还能少写一个函数.

现在我们要修改 \(4\), 观察线段树, 哪些结点和 \(4\) 有关?

可以发现, 首先 \(4\) 在根的左儿子, 然后到右儿子, 再到右儿子.

那么就可以通过区间进行递归修改.

void chg(int x, int l, int r, int I, ll k) {
  if (l == r) { // 区间 l 和 r 相等说明是叶子了
    val[x] += k;
  } else {
    int m(l + r >> 1); // 左右分割点, (l + r) / 2
    if (I <= m) // I 在左子区间
      chg(x << 1, l, m, I, k); // 左儿子 2x
    else // I 在右子区间
      chg(x << 1 | 1, m + 1, r, I, k); // 右儿子 2x + 1
    val[x] = val[x << 1] + val[x << 1 | 1]; // 合并答案
  }
}

区间修改

很容易想到, 把区间递归成单点进行修改, 但是这样的效率太慢了.

void chg(int x, int l, int r, int L, int R, ll k) {
  if (l == r) {
    val[x] += k;
  } else {
    int m(l + r >> 1);
    if (L <= m) // 包含在左子区间
      chg(x << 1, l, m, L, R, k);
    if (m + 1 <= R) // 包含右子区间
      chg(x << 1 | 1, m + 1, r, L, R, k);
    val[x] = val[x << 1] + val[x << 1 | 1]; // 合并答案
  }
}

观察线段树 (这次观察上次树状数组的博客里的图).

要修改 \(1\)\(7\), 哪些结点完全包含 \(1\)\(7\), 不需要往下递归了呢?

可以发现, 递归到 \([1, 4]\)\([5, 6]\)\([7, 7]\) 就不需要往下递归了.

但是此时没有递归到的在区间内的结点的值是错误的.

我们引入一个懒标记.

ll tag[NN << 2]; // 叶子其实无需懒标记, 但是那样还要特判

可以想到懒标记在操作子区间时需要下传.

inline
void pushdown(int x, int l, int r, int m) { // 传入 m 就不用再算了
  val[x << 1] += tag[x] * (m - l + 1); // 左子区间是 l 到 m
  val[x << 1 | 1] += tag[x] * (r - m); // 右子区间是 m + 1 到 r
  tag[x << 1] += tag[x]; // 标记下传
  tag[x << 1 | 1] += tag[x]; // 标记下传
  tag[x] = 0LL; // 清空标记
}

所以区间修改的代码就写出来了.

void chg(int x, int l, int r, int L, int R, ll k) {
  if (L <= l && r <= R) {
    val[x] += k * (r - l + 1); // 加时乘区间长度
    tag[x] += k; // 打标记
  } else {
    int m(l + r >> 1);
    pushdown(x, l, r, m); // 下传标记
    if (L <= m)
      chg(x << 1, l, m, L, R, k);
    if (m + 1 <= R)
      chg(x << 1 | 1, m + 1, r, L, R, k);
    val[x] = val[x << 1] + val[x << 1 | 1];
  }
}

区间查询

学会了修改就很容易想到查询, 合并答案即可.

ll qry(int x, int l, int r, int L, int R) {
  if (L <= l && r <= R) {
    return val[x];
  } else {
    int m(l + r >> 1); l s(0LL);
    pushdown(x, l, r, m);
    if (L <= m)
      s += qry(x << 1, l, m, L, R);
    if (m + 1 <= R)
      s += qry(x << 1 | 1, m + 1, r, L, R);
    return s;
  }
}

例题

A Simple Problem with Integers

VJudge POJ

板子.

线段树2

VJudge LuoGu

这个不算难, 就是先下放乘标记再下放加标记, 加标记在乘时也要乘.

给一个pushdown(x, l, r, m)吧.

inline
void addmul(int x, int l, int r, ll f, ll g) {
  if (g ^ 1LL) {
    val[x] = val[x] * g % Mod;
    add[x] = add[x] * g % Mod;
    mul[x] = mul[x] * g % Mod;
  }
  if (f ^ 0LL) {
    val[x] = (val[x] + f * (r - l + 1)) % Mod;
    add[x] = (add[x] + f * (r - l + 1)) % Mod;
  }
}

inline
void pushdown(int x, int l, int r, int m) {
  addmul(x << 1, l, m, add[x], mul[x]);
  addmul(x << 1 | 1, m + 1, r, add[x], mul[x]);
  add[x] = 0LL;
  mul[x] = 1LL;
}

无聊的数列

VJudge LuoGu

差分, 单点加首项, 区间加公差, 在下一个减去最后一项, 区间查前缀和.

扶苏的问题

VJudge LuoGu

这个不难, 打两个标记即可.

posted @ 2024-11-09 07:56  指针神教教主Defad  阅读(15)  评论(0编辑  收藏  举报