Segment Trees

線段樹DLC

INDEX

遞迴式

懶標

動態開點

簡報看不懂我也沒辦法

Segment Tree (Recursive)

1. 複習線段樹在幹嘛

2. 遞迴式寫法

Segment Tree

支援各種區間查詢的資結

39
22
17
5
8
6
3
2
7
2
6
13
9
9
8
8
9
10
11
12
13
14
15
1
2
3
4
5
6
7

每個節點都代表一個 "線段",這些節點會組成一顆二元 "樹"

父節點的值都是由兩個小孩推出來的

遞迴式線段樹

遞迴寫法比較好理解(?
一顆線段樹需要:

1. build() 建構

2. modify() 單點改值

3. query() 區間查詢

遞迴式線段樹

1. build()

void build(int id, int l, int r, vector<int> &v) {
    if (l == r) { // 葉節點直接賦值
        a[id] = v[l];
        return;
    }

    int m = (l + r) / 2; 
    
    build(id * 2, l, m, v); 
    // 蓋左子樹 
    build(id * 2 + 1, m + 1, r, v); 
    // 蓋右子樹
    
    a[id] = min(a[id * 2], a[id * 2 + 1]); 
    // 用左子節點跟右子節點回推當前節點
}

遞迴式線段樹

2. modify()

void single_modify(int id, int l, int r, int pos, int val) {
    if (l == r) { // 葉節點直接改值
        a[id] = val;
        return;
    }

    int m = (l + r) / 2;
    if (pos <= m) { // 分治概念,要改的在左邊就改左子樹
        single_modify(id * 2, l, m, pos, val);
    } else { // 改右子樹
        single_modify(id * 2 + 1, m + 1, r, pos, val);
    }

    // 用左右子節點回推當前節點
    a[id] = min(a[id * 2], a[id * 2 + 1]);
}

遞迴式線段樹

3. query()

int range_query(int id, int l, int r, int L, int R) {
    if (L <= l && r <= R) { // 查詢範圍完全包含當前處理範圍
        return a[id]; // 直接回傳答案
    }

    int m = (l + r) / 2, res = INFF;
    if (L <= m) {
        // 查詢範圍包含左子樹,考慮左子樹
        res = min(res, range_query(id * 2, l, m, L, R)); 
    } 
    if (R > m) {
        // 查詢範圍包含右子樹,考慮右子樹
        res = min(res, range_query(id * 2 + 1, m + 1, r, L, R));
    }
    return res;
}

遞迴式線段樹

4. struct 包起來

struct SegTree {
    int a[MAXN * 4 + 10]; // 可以證明 worst case 下需要 4n 才可以存
    
    void build(int id, int l, int r, vector<int> &v) {
        if (l == r) {
            a[id] = v[l];
            return;
        }

        int m = (l + r) / 2;
        build(id * 2, l, m, v);
        build(id * 2 + 1, m + 1, r, v);
        a[id] = min(a[id * 2], a[id * 2 + 1]);
    }

    void single_modify(int id, int l, int r, int pos, int val) {
        if (l == r) {
            a[id] = val;
            return;
        }

        int m = (l + r) / 2;
        if (pos <= m) {
            single_modify(id * 2, l, m, pos, val);
        } else {
            single_modify(id * 2 + 1, m + 1, r, pos, val);
        }
        a[id] = min(a[id * 2], a[id * 2 + 1]);
    }

    int range_query(int id, int l, int r, int L, int R) {
        if (L <= l && r <= R) {
            return a[id];
        }

        int m = (l + r) / 2, res = INFF;
        if (L <= m) {
            res = min(res, range_query(id * 2, l, m, L, R));
        } 
        if (R > m) {
            res = min(res, range_query(id * 2 + 1, m + 1, r, L, R));
        }
        return res;
    }
};

題目

Lazy Propagation

區間改值

懶標線段樹

Range Modify

區間改值

思考一下,如果我們要對一個區間修改

修改一個點要 

$$ O(log  n) $$

Worst case?

$$ O(n  log  n) $$

原地 💥

Range Modify

區間改值

怎麼辦 ?

只好多紀錄一些東西

懶人標記 ✅

Range Modify

區間改值

39
22
17
5
8
6
3
2
7
2
6
13
9
9
8
8
9
10
11
12
13
14
15
1
2
3
4
5
6
7

觀察一下修改 [1, 5] 區間要動到哪些節點

Range Modify

區間改值

39
22
17
5
8
6
3
2
7
2
6
13
9
9
8
8
9
10
11
12
13
14
15
1
2
3
4
5
6
7

觀察一下修改 [1, 5] 區間要動到哪些節點

Range Modify

區間改值

39
22
17
5
8
6
3
2
7
2
6
13
9
9
8
8
9
10
11
12
13
14
15
1
2
3
4
5
6
7

觀察一下修改 [1, 5] 區間要動到哪些節點

這顆子樹

整個都要改欸

Range Modify

區間改值

39
22
17
5
8
6
3
2
7
2
6
13
9
9
8
8
9
10
11
12
13
14
15
1
2
3
4
5
6
7

在節點 2 記錄一個 tag,表達底下這顆子樹都要修改

等我需要子節點資訊時再把 tag 往下推就好了

 

這顆子樹

整個都要改欸

$$ O(log  n) $$

這樣時間複雜度會降回

懶標線段樹

懶標下推 push()

    void push(int id, int l, int r){
        int m=(l+r)/2;
        st[lc(id)]+=lz[id]*(m-l+1);
        st[rc(id)]+=lz[id]*(r-(m+1)+1);
        lz[lc(id)]+=lz[id];
        lz[rc(id)]+=lz[id];
        lz[id]=0;
    }

懶標線段樹

用兩個子節點更新當節點

pull()

    void pull(int id){
        st[id]=st[lc(id)]+st[rc(id)];
    }

懶標線段樹

區間改值

modify()

    void modify(int l, int r, int ql, int qr, int w, int id){
        if(r<ql||l>qr) return;
        if(ql<=l&&r<=qr) {
            st[id]+=w*(r-l+1);
            lz[id]+=w;
            return;
        }
        push(id, l, r);
        int m=(l+r)/2;
        modify(l, m, ql, qr, w, lc(id));
        modify(m+1, r, ql, qr, w, rc(id));
        pull(id);
    }

懶標線段樹

區間查詢

query()

    int query(int l, int r, int ql, int qr, int id){
        if(r<ql||l>qr) return 0;
        if(ql<=l&&r<=qr){
            return st[id];
        }
        push(id, l, r);
        int m=(l+r)/2;
        return query(l, m, ql, qr, lc(id))+query(m+1, r, ql, qr, rc(id));
    }

懶標線段樹

Code (這是查區間和的)

學長對不起我下次一定會先跑過 code

#include <bits/stdc++.h>
using namespace std;
#define int long long
#define pii pair<int,int>
#define pb push_back
#define all(x) x.begin(),x.end()
#define ff first
#define ss second
#define lc(x) x*2
#define rc(x) x*2+1


struct seg{
    int n;
    vector<int> st;
    vector<int> lz;
    seg(int N){
        n=N;
        st.resize(4*n, 0);
        lz.resize(4*n, 0);
    }

    void pull(int id){
        st[id]=st[lc(id)]+st[rc(id)];
    }

    void push(int id, int l, int r){
        int m=(l+r)/2;
        st[lc(id)]+=lz[id]*(m-l+1);
        st[rc(id)]+=lz[id]*(r-(m+1)+1);
        lz[lc(id)]+=lz[id];
        lz[rc(id)]+=lz[id];
        lz[id]=0;
    }

    void modify(int l, int r, int ql, int qr, int w, int id){
        if(r<ql||l>qr) return;
        if(ql<=l&&r<=qr) {
            st[id]+=w*(r-l+1);
            lz[id]+=w;
            return;
        }
        push(id, l, r);
        int m=(l+r)/2;
        modify(l, m, ql, qr, w, lc(id));
        modify(m+1, r, ql, qr, w, rc(id));
        pull(id);
    }

    int query(int l, int r, int ql, int qr, int id){
        if(r<ql||l>qr) return 0;
        if(ql<=l&&r<=qr){
            return st[id];
        }
        push(id, l, r);
        int m=(l+r)/2;
        return query(l, m, ql, qr, lc(id))+query(m+1, r, ql, qr, rc(id));
    }
};


signed main(){
    ios_base::sync_with_stdio(false);cin.tie(0);
    int t;cin>>t;
    while(t--){
        int n, q;cin>>n>>q;
        auto segtree=seg(n);
        while(q--){
            int op;cin>>op;
            if(op==0) {
                int p, q, v;cin>>p>>q>>v;
                segtree.modify(1, n, p, q, v, 1);
            }else{
                int p, q;cin>>p>>q;
                cout<<segtree.query(1, n, p, q, 1)<<'\n';
            }
        }
    }

}

題目

好大一顆線段樹

Code

動態開點線段樹

思考時間

設想一下

 

一如往常的在寫題目

你發現了一題單點改值區間查詢的裸題

定睛一看

 

$$ n \leq 10^9 $$

啊?

 

思考時間

$$ n \leq 10^9 $$

原地 💥

思考時間

所以我們可以

原地 💥

思考時間

所以我們可以

需要用到該節點的時候再開

aka 動態開點

好大一顆線段樹

不過我們不能用迭代式/遞迴式寫

要用指標型線段樹做

 

(因為迭代式跟遞迴式要先開好記憶體)

#include <iostream>
using namespace std;
typedef long long ll;

// 節點結構:僅存儲該區間的總和與子節點指針
struct Node {
    ll sum;
    Node *left, *right;
    Node() : sum(0), left(nullptr), right(nullptr) {}
};

class DynamicSegTree {
private:
    Node* root;
    ll L, R; // 整個線段樹的範圍 [L, R]

public:
    DynamicSegTree(ll L, ll R) : L(L), R(R) {
        root = new Node();
    }

    // 單點更新:將位置 pos 更新為 val
    void update(ll pos, ll val) {
        update(root, L, R, pos, val);
    }

    // 區間查詢:查詢區間 [l, r] 的累積和
    ll query(ll l, ll r) {
        return query(root, L, R, l, r);
    }

private:
    // 遞迴更新:將位置 pos 的值設為 val
    void update(Node* node, ll nl, ll nr, ll pos, ll val) {
        if (nl == nr) { // 到達葉節點
            node->sum = val;
            return;
        }
        ll mid = (nl + nr) / 2;
        if (pos <= mid) {
            if (!node->left) node->left = new Node();
            update(node->left, nl, mid, pos, val);
        } else {
            if (!node->right) node->right = new Node();
            update(node->right, mid + 1, nr, pos, val);
        }
        node->sum = (node->left ? node->left->sum : 0) + (node->right ? node->right->sum : 0);
    }

    // 遞迴查詢:查詢區間 [ql, qr] 的累積和
    ll query(Node* node, ll nl, ll nr, ll ql, ll qr) {
        if (!node || ql > nr || qr < nl) return 0; // 超出範圍
        if (ql <= nl && nr <= qr) return node->sum; // 完全覆蓋
        ll mid = (nl + nr) / 2;
        return query(node->left, nl, mid, ql, qr) + query(node->right, mid + 1, nr, ql, qr);
    }
};

int main() {
    DynamicSegTree seg(1, 1000000000);

    seg.update(5, 10);
    seg.update(10, 20);
    seg.update(7, 15);

    cout << "Sum[1,10]: " << seg.query(1, 10) << endl; // 10 + 15 + 20 = 45
    cout << "Sum[6,8]: " << seg.query(6, 8) << endl;   // 15 (at 7)
    cout << "Sum[10,10]: " << seg.query(10, 10) << endl; // 20

    return 0;
}

動態開點線段樹 by cjtsai 的 GPT Plus

不然網路上找的扣都醜的一批

題目 & Live Coding

掰掰

More Segment Tree

By wen Ian

More Segment Tree

  • 229