不知道是基礎還是進階資料結構(一)
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]\)的答案
有四種情況
- \(ql = l\) 且 \(qr =r\) :直接拿現在這個節點的答案就好ㄌ
- 要查詢的區間在左邊(\(qr \le 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\) \((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)\)