不知道是基礎還是進階資料結構(一)

slides : mouyilai

edit : lemon

關於蔗糖課

高三最近比較忙QAQ

所以跟學姊借了一份簡報。

大部分我想講的應該會直接畫在白板上(?

 

然後就是

線段樹是一個很重要的技術

一定要學好ㄛ!

今天的內容

  • 沒有懶標的線段樹
  • BIT

Range
Maximum/Minimum
Query

給你一個長度為 \(N\) 的序列和 \(Q\) 次詢問,有兩種詢問

1. 把 index 為\(x\) 的值改成 \(k\) 

2. 查詢 \([a, b]\) 區間的最小值

(\(N, Q \leq 2\times 10^5\))

最直接的想法: 全部掃過一次

複雜度 \(O(NQ)\) 成功 TLE

殼以用線段樹 讚

第二直接的想法: 前綴min

然後改值就爛ㄌ

線段樹

畫質燒雞的線斷樹

仙貝知識 - 樹

  • Tree
  • 圖的一種
  • 長得像這樣 →
  • 重要的性質是每個點之間都聯通
  • 而且每兩個點之間的路徑只有一條
  • 但這在線段樹裡面不重要
  • 下禮拜會詳細講

仙貝知識 - 完整二元樹

  • Complete Binary Tree
  • 反正是樹ㄉ一種
  • 每個點最多只有兩個子節點 而且各層節點全滿,除了最後一層,最後一層節點全部靠左。
  • 所以第 \(k\) 層最多只有 \(2^k\) 個節點
  • 用一個陣列來記錄每一個節點
  • 一個節點 \(x\) 的左小孩是 \(2x\) 右小孩是 \(2x + 1\)

仙貝知識 - 分治

  • 把一個問題分成小問題 然後再 merge 起來

然後我們就可以開始蓋線段樹ㄌ

  • 蓋一棵二元樹 用一個節點來維護每一個區間的資訊
  • 用陣列存的話要開空間是\(4n\)的陣列
  • 一個index是 \(x\) 的節點的左小孩的 index 是 \(2x\) 右小孩的 index 是 \(2x+1\) (1-base)
  • 如果一個節點要存的咚咚很多可以開一個struct

\(a= \{3,\ 2,\ 4,\ 5,\ 1,\ 1,\ 5,\ 3\}\)

蓋線段樹

Pull(拉)

vector <ll> segtree, a;
void pull(int x) { //用兩個子節點來更新現在的節點的答案
    segtree[x] = min(segtree[2 * x], segtree[2 * x + 1]);
}

利用兩個子節點更新答案!

code

void build(int l, int r, int x) { //用index是x的節點來存[l, r]的答案
    if(l == r) { //葉節點
        segtree[x] = a[l];
        return;
    }
    int mid = (l + r) / 2;
    build(l, mid, 2 * x), build(mid + 1, r, 2 * x + 1);
    pull(x); //要記得更新!
}

單點修改

void modify(int p, int v, int l, int r, int x) { 
    //把index為p的節點的值改為v 現在在的節點是index為x,存的是[l, r]的答案
    if(l == r) { //找到ㄌ
        segtree[x] = v;
        return;
    }
    int mid = (l + r) / 2;
    if(p <= mid) modify(p, v, l, mid, 2 * x); //往左子節點遞迴
    else modify(p, v, mid + 1, r, 2 * x + 1); //往右子節點遞迴
    pull(x); //用左右子節點的答案更新x的答案
} 

每次從中間切,如果要修改的點在左子樹就往左邊遞迴,在右邊就往右邊遞迴

區間查詢

假設我現在在的節點存的是\([l, r]\)的答案,\(mid= \lfloor \frac{l+r}{2}\rfloor\)

我想查詢\([ql, qr]\)的答案

有四種情況

  1. \(ql = l\) 且 \(qr =r\) :直接拿現在這個節點的答案就好ㄌ
  2. 要查詢的區間在左邊(\(qr \le mid\)) :往左邊節點遞迴
  3. 要查詢的區間在右邊(\(mid < ql\)):往右邊節點遞迴
  4. 要查詢的區間跨越左右兩邊:往左右兩邊遞迴,再把答案合併

區間查詢

ll query(int ql, int qr, int l, int r, int x) {
    if(ql == l && qr == r) return segtree[x];
    int mid = (l + r) / 2;
    if(qr <= mid) return query(ql, qr, l, mid, 2 * x);
    else if(mid < ql) return query(ql, qr, mid + 1, r, 2 * x + 1);
    return min(query(ql, mid, l, mid, 2 * x), query(mid + 1, qr, mid + 1, r, 2 * x + 1));
}

好耶做完ㄌ

時間複雜度

  • 因為這是一棵二元樹所以它的深度是\(logN\)
  • 每次詢問最多只會從樹根走到葉節點
  • 所以一次詢問只會用\(O(logN)\)
  • 有\(q\)次詢問所以總共是\(O(QlogN)\)

完整的code

using namespace std;
#include <bits/stdc++.h>
typedef long long ll;
 
vector <ll> segtree, a;
void pull(int x) {
    segtree[x] = min(segtree[2 * x], segtree[2 * x + 1]);
}
void build(int l, int r, int x) {
    if(l == r) {
        segtree[x] = a[l];
        return;
    }
    int mid = (l + r) / 2;
    build(l, mid, 2 * x), build(mid + 1, r, 2 * x + 1);
    pull(x);
}
void modify(int p, int v, int l, int r, int x) {
    if(l == r) {
        segtree[x] = v;
        return;
    }
    int mid = (l + r) / 2;
    if(p <= mid) modify(p, v, l, mid, 2 * x);
    else modify(p, v, mid + 1, r, 2 * x + 1);
    pull(x);
} 
ll query(int ql, int qr, int l, int r, int x) {
    if(ql == l && qr == r) return segtree[x];
    int mid = (l + r) / 2;
    if(qr <= mid) return query(ql, qr, l, mid, 2 * x);
    else if(mid < ql) return query(ql, qr, mid + 1, r, 2 * x + 1);
    return min(query(ql, mid, l, mid, 2 * x), query(mid + 1, qr, mid + 1, r, 2 * x + 1));
}
int main() {
    int n, q;
    cin >> n >> q;
    segtree.resize(4 * n);
    a.resize(n + 1);
    for(int i = 1; i <= n; ++i) cin >> a[i];
    build(1, n, 1);
    while(q--) {
        int op, a, b;
        cin >> op >> a >> b;
        if(op == 1) modify(a, b, 1, n, 1);
        else cout << query(a, b, 1, n, 1) << '\n';
    }
    return 0;
}

完整的code(指標型線段樹)

using namespace std;
#include <bits/stdc++.h>
typedef long long ll;
const ll INF = 1e18;
 
struct Node{
    Node *lc, *rc;
    ll val;
    void pull() {
        val = min(lc->val, rc->val);
    }
};
Node *root = nullptr;
Node *build(int l, int r, vector <int> &a) {
    Node *nd = new Node();
    if(l == r) {
        nd->lc = nd->rc = nullptr;
        nd->val = a[l];
    }
    else {
        int mid = (l + r) / 2;
        nd->lc = build(l, mid, a), nd->rc = build(mid + 1, r, a);
        nd->pull();
    }
    return nd;
}
void modify(Node *nd, int p, int v, int l, int r) {
    if(l == r) {
        nd->val = v;
        return;
    }
    int mid = (l + r) / 2;
    if(p <= mid) modify(nd->lc, p, v, l, mid);
    else modify(nd->rc, p, v, mid + 1, r);
    nd->pull();
}
ll query(Node *nd, int ql, int qr, int l, int r) {
    if(r < ql || qr < l) return INF;
    if(ql <= l && r <= qr) return nd->val;
    int mid = (l + r) / 2;
    return min(query(nd->lc, ql, qr, l, mid), query(nd->rc, ql, qr, mid + 1, r));
}

int main() {
    int n, q;
    cin >> n >> q;
    vector <int> a(n + 1);
    for(int i = 1; i <= n; ++i) cin >> a[i];
    root = build(1, n, a);
    while(q--) {
        int op, a, b;
        cin >> op >> a >> b;
        if(op == 1) modify(root, a, b, 1, n);
        else cout << query(root, a, b, 1, n) << '\n';
    }
    return 0;
}

題目

BIT

線段樹減肥ㄌ

BIT

什麼是BIT

  • Binary Indexed Tree
  • Fenwick Tree
  • 可以做帶修改前綴和
  • 其實只要有結合律都可以做(sum, xor, gcd......)
  • 常數跟空間都比線段樹小
  • 好寫 讚

lowbit

  • 一個數字轉為二進位後, 最後一個1的位置所代表的數值
  • 因為一些奇妙的黑魔法 所以 \(lowbit(x)=x \&(-x)\)
  • \(lowbit(8)=8\)         \((8_{10} = 1000_{2})\)
  • \(lowbit(12)=4\)       \((12_{10} = 1100_{2})\)

例子

BIT

  • 如果節點的index是\(x\),這個節點會從 index \(x\)開始存\(lowbit(x)\)個值的答案,aka.存\([x - lowbit(x) + 1,x]\)的答案
  • 要查詢\([ql, qr]\)的和的時候 就查\(qr的前綴和 - (ql - 1) 的前綴和\)
  • 修改就痾修改

單點修改

把index為3的值增加1

\(4=3 + lowbit(3)\)

=>  ++BIT[3], ++BIT[4], ++BIT[8]

\(8=4 + lowbit(4)\)

每次往右走\(lowbit(x)\)

查詢前綴和

查詢index14的前綴和

\(8=12 + lowbit(12)\)

=>  BIT[14] + BIT[12] + BIT[8]

\(12=14 - lowbit(14)\)

每次往左走\(lowbit(x)\)

code

void modify(int p, int v) {
    for(; p <= n; p += p & -p) 
        BIT[p] += v;
}
ll query(int q) {
    ll ret = 0;
    for(; q > 0; q -= q & -q) 
    	ret += BIT[q];
    return ret;
}

超好寫 出錯機率大概是\(10^{-9}\)

複雜度

因為每次都往你ㄉlowbit值走,所以每次查詢跟修改的時間複雜度都是\(O(logn)\)

空間複雜度: 開長度為\(n\)的陣列 空間複雜度\(O(n)\)

coding複雜度: \(O(1)\)

題目