不知道是基礎還是進階資料結構(一)
slides : mouyilai
edit : lemon
關於蔗糖課
高三最近比較忙QAQ
所以跟學姊借了一份簡報。
大部分我想講的應該會直接畫在白板上(?
然後就是
線段樹是一個很重要的技術
一定要學好ㄛ!
今天的內容
- 沒有懶標的線段樹
- BIT
Range
Maximum/Minimum
Query
給你一個長度為 N 的序列和 Q 次詢問,有兩種詢問
1. 把 index 為x 的值改成 k
2. 查詢 [a,b] 區間的最小值
(N,Q≤2×105)
最直接的想法: 全部掃過一次
複雜度 O(NQ) 成功 TLE
殼以用線段樹 讚
第二直接的想法: 前綴min
然後改值就爛ㄌ
線段樹
畫質燒雞的線斷樹

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

仙貝知識 - 完整二元樹
- Complete Binary Tree
- 反正是樹ㄉ一種
- 每個點最多只有兩個子節點 而且各層節點全滿,除了最後一層,最後一層節點全部靠左。
- 所以第 k 層最多只有 2k 個節點
- 用一個陣列來記錄每一個節點
- 一個節點 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=⌊2l+r⌋
我想查詢[ql,qr]的答案
有四種情況
- ql=l 且 qr=r :直接拿現在這個節點的答案就好ㄌ
- 要查詢的區間在左邊(qr≤mid) :往左邊節點遞迴
- 要查詢的區間在右邊(mid<ql):往右邊節點遞迴
- 要查詢的區間跨越左右兩邊:往左右兩邊遞迴,再把答案合併
區間查詢
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 (810=10002)
- lowbit(12)=4 (1210=11002)
例子
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)
題目
不知道是基礎還是進階資料結構(一) slides : mouyilai edit : lemon
線段樹&BIT
By lemonilemon
線段樹&BIT
- 405