线段树

1 线段树概念

线段树 ( S e g m e n t   T r e e ) (Segment\ Tree) (Segment Tree) O I OI OI 中的常用算法。线段树是一种二叉搜索树,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。且时间复杂度仅 Θ ( log ⁡ n ) \varTheta(\log n) Θ(logn)

2 线段树操作

2.1 建树

对于一个区间,我们可以把它用线段树存储。可以把区间 [ 1 , 4 ] [1,4] [1,4] 用如图所示的方式存储:

11 线段树示意

也就是说,如果要求 [ 1 , 4 ] [1,4] [1,4] 的区间和,就可以通过把这个节点的两个儿子 [ 1 , 2 ] [1,2] [1,2] [ 3 , 4 ] [3,4] [3,4] 的权值相加得到。推导到一般式,即

t r e e i . s u m = t r e e i × 2 . s u m + t r e e i × 2 + 1 . s u m tree_i.sum = tree_{i \times 2}.sum + tree_{i \times 2 + 1}.sum treei.sum=treei×2.sum+treei×2+1.sum

struct segment_tree{
    int sum, tag = 0, l, r;
}tree[N << 2];
inline void build(int i, int l, int r) {
    tree[i].l = l, tree[i].r = r;
    if (l == r) {
        tree[i].sum = a[l];
        return;
    } 
    int mid = (l + r) >> 1;
    build(i << 1, l, mid);
    build(i << 1 | 1, mid + 1, r);
    tree[i].sum = tree[i << 1].sum + tree[i << 1 | 1].sum;
}

注意线段树要开四倍空间。

2.2 区间修改

对给定区间每个数增加 k k k。这个似乎有一点棘手,我们可以引入一个新的懒标记 ( L a z y T a g ) (Lazy Tag) (LazyTag)。一个点的懒标记代表它的区间上所有点应当做出的修改。

§ 1 \texttt§1 §1 中的图为例,例如修改 [ 1 , 3 ] [1,3] [1,3] 的值,从 [ 1 , 4 ] [1,4] [1,4] 向下找,它的左儿子 [ 1 , 2 ] [1,2] [1,2] 显然包含在所要修改的区间之内;它的右儿子 [ 3 , 4 ] [3,4] [3,4] 区间过大,从它的左儿子接着找: [ 3 , 3 ] [3,3] [3,3] 在区间中,而 [ 4 , 4 ] [4,4] [4,4] 不在区间中。所以,只需要把 [ 1.2 ] [1.2] [1.2] [ 3 , 3 ] [3,3] [3,3] 标记。

推导到一般地,如果区间包含在修改区间中,那么它需要打标记;如果不包含,考虑儿子:左儿子在区间中,找左儿子;右儿子在区间中,找右儿子。

如果单纯这样想,显然有一个巨大的纰漏:比如我们修改了 [ 1 , 3 ] [1,3] [1,3],然后通过查询操作查询了 [ 1 , 3 ] [1,3] [1,3] 的区间和,没有问题;但如果此时查询 [ 2 , 4 ] [2,4] [2,4],因为 [ 2 , 2 ] [2,2] [2,2] 没有被打标记,所以会少加一个 k k k

对于一个区间,如果整个区间都要修改,那么首先把点的权值增加 k × ( t r e e i . r − t r e e i . l + 1 ) k \times (tree_i.r - tree_i.l + 1) k×(treei.rtreei.l+1)。如果并不是都要修改,这时我们需要一个pushdown操作。所谓pushdown,就是把自己的懒标记清零,再给自己的儿子标记上。

inline void pushdown(int i) {
    if (tree[i].tag) {
        tree[i << 1].tag += tree[i].tag;
        tree[i << 1 | 1].tag += tree[i].tag;
        tree[i << 1].sum += tree[i].tag * (tree[i << 1].r - tree[i << 1].l + 1);
        tree[i << 1 | 1].sum += tree[i].tag * (tree[i << 1 | 1].r - tree[i << 1 | 1].l + 1);
        tree[i].tag = 0;
    }
}

其他的 t a g tag tag 不为 0 0 0 的区间在后面区间查询的时候顺便更新即可。区间修改的代码:

inline void add(int i, int l, int r, int k) {
    if (tree[i].l >= l && tree[i].r <= r) {
        tree[i].sum += k * (tree[i].r - tree[i].l + 1);
        tree[i].tag += k;
        return;
    }
    pushdown(i);
    if (tree[i << 1].r >= l) 
        add(i << 1, l, r, k);
    if (tree[i << 1 | 1].l <= r)
        add(i << 1 | 1, l, r, k);
    tree[i].sum = tree[i << 1].sum + tree[i << 1 | 1].sum;
}

2.3 区间查询

思路类似,注意pushdown即可。

inline int search(int i, int l, int r) {
    if (tree[i].l >= l && tree[i].r <= r) 
        return tree[i].sum;
    pushdown(i);
    int ans = 0;
    if (tree[i << 1].r >= l) 
        ans += search(i << 1, l, r);
    if (tree[i << 1 | 1].l <= r)
        ans += search(i << 1 | 1, l, r);
    return ans;
}

2.4 练习题目

【练习 2.1】P3372 【模板】线段树 1 - 洛谷 | 计算机科学教育新生态

题解

线段树模板题(注意要开 long   long \texttt{long long} long long)。

#include <bits/stdc++.h>
#define endl '\n'
using namespace std;
typedef long long ll;
const ll N = 2e5 + 10;
ll n, m, a[N];
struct segment_tree{
    ll sum, tag = 0, l, r;
}tree[N << 2];
inline void build(ll i, ll l, ll r) {
    tree[i].l = l, tree[i].r = r;
    if (l == r) {
        tree[i].sum = a[l];
        return;
    } 
    ll mid = (l + r) >> 1;
    build(i << 1, l, mid);
    build(i << 1 | 1, mid + 1, r);
    tree[i].sum = tree[i << 1].sum + tree[i << 1 | 1].sum;
}
inline void pushdown(ll i) {
    if (tree[i].tag) {
        tree[i << 1].tag += tree[i].tag;
        tree[i << 1 | 1].tag += tree[i].tag;
        tree[i << 1].sum += tree[i].tag * (tree[i << 1].r - tree[i << 1].l + 1);
        tree[i << 1 | 1].sum += tree[i].tag * (tree[i << 1 | 1].r - tree[i << 1 | 1].l + 1);
        tree[i].tag = 0;
    }
}
inline void add(ll i, ll l, ll r, ll k) {
    if (tree[i].l >= l && tree[i].r <= r) {
        tree[i].sum += k * (tree[i].r - tree[i].l + 1);
        tree[i].tag += k;
        return;
    }
    pushdown(i);
    if (tree[i << 1].r >= l) 
        add(i << 1, l, r, k);
    if (tree[i << 1 | 1].l <= r)
        add(i << 1 | 1, l, r, k);
    tree[i].sum = tree[i << 1].sum + tree[i << 1 | 1].sum;
}
inline ll search(ll i, ll l, ll r) {
    if (tree[i].l >= l && tree[i].r <= r) 
        return tree[i].sum;
    pushdown(i);
    ll ans = 0;
    if (tree[i << 1].r >= l) 
        ans += search(i << 1, l, r);
    if (tree[i << 1 | 1].l <= r)
        ans += search(i << 1 | 1, l, r);
    return ans;
}
int main() {
    cin >> n >> m;
    for (ll i = 1; i<= n; i++) 
        cin >> a[i];
    build(1, 1, n);
    while (m--) {
        ll op, x, y, k;
        cin >> op;
        if (op == 1) {
            cin >> x >> y >> k;
            add(1, x, y, k);
        } else {
            cin >> x >> y;
            cout << search(1, x, y) << endl;
        }
    }
    return 0;
}

3 线段树进阶

3.1 乘法线段树

如果线段树只有乘法操作,那么只要让懒标记变成乘法,然后 tree[i].sum *= k 即可。

但如果是有乘法也有加法,我们需要考虑先加还是先乘。

把懒标记分成加法标记 ( p l z ) (plz) (plz)乘法标记 ( m l z ) (mlz) (mlz)。对于 m l z mlz mlz,直接乘上父亲的标记即可,而对于 p l z plz plz,需要把原先的 p l z plz plz 乘上父亲的 m l z mlz mlz 再加上父亲的 p l z plz plz

【练习 3.1】P3373 【模板】线段树 2 - 洛谷 | 计算机科学教育新生态

题解

乘法线段树模板题(注意要开 long   long \texttt{long long} long long 和取模)。

#include <bits/stdc++.h>
#define endl '\n'
using namespace std;
typedef long long ll;
const ll N = 2e5 + 10;
ll n, m, q, a[N];
struct segment_tree{
    ll sum, plz, mlz, l, r;
}tree[N << 2];
inline ll mod(ll x){
    return (x % m + m) % m;
}
inline void build(ll i, ll l, ll r) {
    tree[i].l = l, tree[i].r = r;
    tree[i].plz = 0, tree[i].mlz = 1;
    if (l == r) {
        tree[i].sum = a[l];
        return;
    } 
    ll mid = (l + r) >> 1;
    build(i << 1, l, mid);
    build(i << 1 | 1, mid + 1, r);
    tree[i].sum = mod(tree[i << 1].sum + tree[i << 1 | 1].sum);
}
inline void pushdown(ll i) {
    if (tree[i].plz == 0 && tree[i].mlz == 1) 
        return;
    int mlz0 = tree[i].mlz, plz0 = tree[i].plz;
    tree[i << 1].sum = mod(tree[i << 1].sum * mlz0 + plz0 * (tree[i << 1].r - tree[i << 1].l + 1));
    tree[i << 1].mlz = mod(tree[i << 1].mlz * mlz0);
    tree[i << 1].plz = mod(tree[i << 1].plz * mlz0 + plz0);
    tree[i << 1 | 1].sum = mod(tree[i << 1 | 1].sum * mlz0 + plz0 * (tree[i << 1 | 1].r - tree[i << 1 | 1].l + 1));
    tree[i << 1 | 1].mlz = mod(tree[i << 1 | 1].mlz * mlz0);
    tree[i << 1 | 1].plz = mod(tree[i << 1 | 1].plz * mlz0 + plz0);
    tree[i].mlz = 1, tree[i].plz = 0;
}
inline void add1(ll i, ll l, ll r, ll k) {
    if (tree[i].l >= l && tree[i].r <= r) {
        tree[i].sum = mod(tree[i].sum * k);
        tree[i].mlz = mod(tree[i].mlz * k);
        tree[i].plz = mod(tree[i].plz * k);
        return;
    }
    pushdown(i);
    if (tree[i << 1].r >= l) 
        add1(i << 1, l, r, k);
    if (tree[i << 1 | 1].l <= r)
        add1(i << 1 | 1, l, r, k);
    tree[i].sum = mod(tree[i << 1].sum + tree[i << 1 | 1].sum);
}
inline void add2(ll i, ll l, ll r, ll k) {
    if (tree[i].l >= l && tree[i].r <= r) {
        tree[i].sum = mod(tree[i].sum + k * (tree[i].r - tree[i].l + 1));
        tree[i].plz = mod(tree[i].plz + k);
        return;
    }
    pushdown(i);
    if (tree[i << 1].r >= l) 
        add2(i << 1, l, r, k);
    if (tree[i << 1 | 1].l <= r)
        add2(i << 1 | 1, l, r, k);
    tree[i].sum = mod(tree[i << 1].sum + tree[i << 1 | 1].sum);
}
inline ll search(ll i, ll l, ll r) {
    if (tree[i].l >= l && tree[i].r <= r) 
        return tree[i].sum;
    pushdown(i);
    ll ans = 0;
    if (tree[i << 1].r >= l) 
        ans = mod(ans + search(i << 1, l, r));
    if (tree[i << 1 | 1].l <= r)
        ans = mod(ans + search(i << 1 | 1, l, r));
    return ans;
}
int main() {
    cin >> n >> q >> m;
    for (ll i = 1; i <= n; i++) 
        cin >> a[i];
    build(1, 1, n);
    while (q--) {
        ll op, x, y, k;
        cin >> op;
        if (op == 1) {
            cin >> x >> y >> k;
            add1(1, x, y, k);
        }
        else if (op == 2) {
            cin >> x >> y >> k;
            add2(1, x, y, k);
        } else {
            cin >> x >> y;
            cout << search(1, x, y) << endl;
        }
    }
    return 0;
}

* 补充:快读快写

模板如下:

template<typename T>inline void read(T &x) {
    bool f = 1;x = 0;char ch = getchar();
    while (ch < '0' || ch > '9') {if (ch == '-') f = !f;ch = getchar(); }
    while (ch >= '0' && ch <= '9') {x = (x << 1) + (x << 3) + (ch ^ 48);ch = getchar();}
    x = (f ? x : -x);return;
}
template<typename T>inline void write(T x) {
    if (x < 0) putchar('-'), x = -x;
    if (x > 9) write(x / 10);
    putchar(x % 10 + '0');return;
}

快写不如printf和优化过的cout快,所以还是建议使用prinf

4 End

参考文章:

感谢yes_NTHappy_WA_Day同学的指导。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值