线段树 学习笔记


Part 1. 线段树简介

线段树是算法竞赛中常用的用来维护区间信息的数据结构。
线段树可以在 \(O(\log n)\) 的时间复杂度内实现单点修改、区间修改、区间查询(区间求和,求区间最大值,求区间最小值)等操作。

线段树的操作和树状数组很相似,不过线段树可以干一些树状数组干不了的,但后者更快些。

线段树可以看作是一棵满二叉树,尽管有些节点是空的。

图片来源于网络

根据完全二叉树的性质,\(i\) 的左儿子就是 \(i \times 2\),右儿子就是 \(i \times 2+1\)

Part 2. 建树(build)

首先我们得对要操作的序列建树,才能进行更多操作。

由于完全二叉树的性质,可以直接用一个一维数组存线段树。

特别注意,线段树数组要开 \(4\) 倍的空间,至于为什么,画一个序列长度 \(10\) 的线段树就知道了。

int a[MAXN];
int tree[MAXN << 2];
inline int ls(int p) { return p << 1; }//左儿子
inline int rs(int p) { return p << 1 | 1; }//右儿子
inline void push_up(int p) { tree[p] = tree[ls(p)] + tree[rs(p)]; }//由左右儿子来更新当前区间的值
void build(int p, int l, int r) {
    if (l == r) { tree[p] = a[l]; return; }
    int mid = l + r >> 1;
    build(ls(p), l, mid);
    build(rs(p), mid + 1, r);
    push_up(p);//回溯时才更新
}

push_up 很灵活,这里实现的是区间求和,所以将左右区间加起来,当然可以改成最小值或最大值也没问题。

根据这个特性,我们发现线段树只能维护满足结合律的东西。

Part 3. 更新(update)

线段树的更新非常特别,用了一个叫懒标记(Lazy tag)的东西。

其思想是,当用到这个节点时,才去更新,而不是一来就更新(不然一样很慢)。

void push_down(int p, int l, int r) {
    int mid = l + r >> 1;
    tag[ls(p)] += tag[p];
    tree[ls(p)] += (mid - l + 1) * tag[p];
    tag[rs(p)] += tag[p];
    tree[rs(p)] += (r - (mid + 1) + 1) * tag[p];
    tag[p] = 0;
}
void update(int ql, int qr, int l, int r, int p, int k) {
    if (ql <= l && r <= qr) {
        tag[p] += k;
        tree[p] += (r - l + 1) * k;
        return;
    }
    push_down(p, l, r);
    int mid = l + r >> 1;
    if (ql <= mid)update(ql, qr, l, mid, ls(p), k);
    if (qr > mid)update(ql, qr, mid + 1, r, rs(p), k);
    push_up(p);
}

个人认为,push_down 操作是线段树最优美的操作,一开始比较难理解,很正常。

特别注意:push_down 操作仅适用于(大概吧)区间求和类的问题,对于求区间最小最大值的,不要使用 push_down

Part 4. 查询(query)

查询的代码和更新类似。

int query(int ql, int qr, int l, int r, int p) {
    if (ql <= l && r <= qr) { return tree[p]; }
    push_down(p, l, r);//记得,用到该节点时要 push_down
    int mid = l + r >> 1;
    int res = 0;
    if (ql <= mid)res += query(ql, qr, l, mid, ls(p));
    if (qr > mid)res += query(ql, qr, mid + 1, r, rs(p));
    return res;
}

特别注意:query 操作需要 push_down,但不需要 push_up

Part 5. 高清无码标程

线段树的模板:

int a[MAXN];
int tree[MAXN << 2], tag[MAXN << 2];
inline int ls(int p) { return p << 1; }
inline int rs(int p) { return p << 1 | 1; }
inline void push_up(int p) { tree[p] = tree[ls(p)] + tree[rs(p)]; }
void build(int p, int l, int r) {
    if (l == r) { tree[p] = a[l]; return; }
    int mid = l + r >> 1;
    build(ls(p), l, mid);
    build(rs(p), mid + 1, r);
    push_up(p);
}
void push_down(int p, int l, int r) {
    int mid = l + r >> 1;
    tag[ls(p)] += tag[p];
    tree[ls(p)] += (mid - l + 1) * tag[p];
    tag[rs(p)] += tag[p];
    tree[rs(p)] += (r - (mid + 1) + 1) * tag[p];
    tag[p] = 0;
}
void update(int ql, int qr, int l, int r, int p, int k) {
    if (ql <= l && r <= qr) {
        tag[p] += k;
        tree[p] += (r - l + 1) * k;
        return;
    }
    push_down(p, l, r);
    int mid = l + r >> 1;
    if (ql <= mid)update(ql, qr, l, mid, ls(p), k);
    if (qr > mid)update(ql, qr, mid + 1, r, rs(p), k);
    push_up(p);
}
int query(int ql, int qr, int l, int r, int p) {
    if (ql <= l && r <= qr) { return tree[p]; }
    push_down(p, l, r);
    int mid = l + r >> 1;
    int res = 0;
    if (ql <= mid)res += query(ql, qr, l, mid, ls(p));
    if (qr > mid)res += query(ql, qr, mid + 1, r, rs(p));
    return res;
}