1585 字
8 分鐘
線段樹 (Segment Tree) 完全攻略
線段樹 (Segment Tree) 完全攻略
當你需要頻繁處理「區間查詢」和「區間更新」時,線段樹就是你最好的夥伴!
🎯 什麼是線段樹?
線段樹是一種二元樹資料結構,專門用來解決區間查詢問題。想像一下,如果你有一個陣列,需要頻繁地:
- 查詢某個區間的最小值、最大值或總和
- 更新某個位置的值
- 處理區間更新操作
在沒有線段樹的情況下,每次查詢都需要 (O(n)) 的時間複雜度。但是使用線段樹,我們可以將時間複雜度降低到 (O(\log n))!
NOTE線段樹的核心思想是「分治法」- 將大區間拆分成小區間,遞迴處理。
📊 線段樹的結構
基本概念
假設我們有一個陣列 [1, 3, 5, 7, 9, 11],要建立一個求區間總和的線段樹:
[0,5] = 36
/ \
[0,2] = 9 [3,5] = 27
/ \ / \
[0,1] = 4 [2,2] = 5 [3,4] = 16 [5,5] = 11
/ \ / \
[0,0] = 1 [1,1] = 3 [3,3] = 7 [4,4] = 9每個節點代表一個區間,儲存該區間的總和(或其他統計值)。
數學性質
對於一個大小為 (n) 的陣列:
- 線段樹的高度為 (\lceil \log_2 n \rceil + 1)
- 總節點數最多為 (4n)(為了方便實作,通常開 4 倍空間)
- 葉子節點數量為 (n)
💻 線段樹實作
基礎結構
class SegmentTree {
private:
vector<int> tree;
int n;
void build(vector<int>& arr, int node, int start, int end) {
if (start == end) {
// 葉子節點
tree[node] = arr[start];
} else {
int mid = (start + end) / 2;
// 遞迴建立左右子樹
build(arr, 2*node, start, mid);
build(arr, 2*node+1, mid+1, end);
// 合併左右子樹的結果
tree[node] = tree[2*node] + tree[2*node+1];
}
}
public:
SegmentTree(vector<int>& arr) {
n = arr.size();
tree.resize(4 * n);
build(arr, 1, 0, n-1);
}
};TIP我們使用編號從 1 開始的完全二元樹,這樣節點
i的左子節點是2*i,右子節點是2*i+1。
區間查詢操作
int query(int node, int start, int end, int l, int r) {
if (r < start || end < l) {
// 完全不重疊
return 0;
}
if (l <= start && end <= r) {
// 完全包含
return tree[node];
}
// 部分重疊,遞迴查詢
int mid = (start + end) / 2;
int p1 = query(2*node, start, mid, l, r);
int p2 = query(2*node+1, mid+1, end, l, r);
return p1 + p2;
}
// 公開介面
int rangeSum(int l, int r) {
return query(1, 0, n-1, l, r);
}單點更新操作
void update(int node, int start, int end, int idx, int val) {
if (start == end) {
// 找到要更新的葉子節點
tree[node] = val;
} else {
int mid = (start + end) / 2;
if (idx <= mid) {
// 更新左子樹
update(2*node, start, mid, idx, val);
} else {
// 更新右子樹
update(2*node+1, mid+1, end, idx, val);
}
// 更新當前節點
tree[node] = tree[2*node] + tree[2*node+1];
}
}
// 公開介面
void pointUpdate(int idx, int val) {
update(1, 0, n-1, idx, val);
}🔧 完整實作範例
#include <iostream>
#include <vector>
using namespace std;
class SegmentTree {
private:
vector<int> tree;
int n;
void build(vector<int>& arr, int node, int start, int end) {
if (start == end) {
tree[node] = arr[start];
} else {
int mid = (start + end) / 2;
build(arr, 2*node, start, mid);
build(arr, 2*node+1, mid+1, end);
tree[node] = tree[2*node] + tree[2*node+1];
}
}
int query(int node, int start, int end, int l, int r) {
if (r < start || end < l) return 0;
if (l <= start && end <= r) return tree[node];
int mid = (start + end) / 2;
int p1 = query(2*node, start, mid, l, r);
int p2 = query(2*node+1, mid+1, end, l, r);
return p1 + p2;
}
void update(int node, int start, int end, int idx, int val) {
if (start == end) {
tree[node] = val;
} else {
int mid = (start + end) / 2;
if (idx <= mid) {
update(2*node, start, mid, idx, val);
} else {
update(2*node+1, mid+1, end, idx, val);
}
tree[node] = tree[2*node] + tree[2*node+1];
}
}
public:
SegmentTree(vector<int>& arr) {
n = arr.size();
tree.resize(4 * n);
build(arr, 1, 0, n-1);
}
int rangeSum(int l, int r) {
return query(1, 0, n-1, l, r);
}
void pointUpdate(int idx, int val) {
update(1, 0, n-1, idx, val);
}
};
int main() {
vector<int> arr = {1, 3, 5, 7, 9, 11};
SegmentTree st(arr);
cout << "區間 [1, 3] 的總和: " << st.rangeSum(1, 3) << endl; // 15
cout << "區間 [2, 5] 的總和: " << st.rangeSum(2, 5) << endl; // 32
st.pointUpdate(1, 10); // 將 index 1 的值改為 10
cout << "更新後區間 [1, 3] 的總和: " << st.rangeSum(1, 3) << endl; // 22
return 0;
}🚀 進階應用
1. 區間最大值查詢
// 修改合併操作
tree[node] = max(tree[2*node], tree[2*node+1]);
// 查詢時返回最大值
return max(p1, p2);2. 區間最小值查詢
// 修改合併操作
tree[node] = min(tree[2*node], tree[2*node+1]);
// 查詢時返回最小值
return min(p1, p2);3. 懶惰標記 (Lazy Propagation)
當需要處理區間更新時,我們使用懶惰標記來避免每次都更新所有節點:
class LazySegmentTree {
private:
vector<int> tree, lazy;
void push(int node, int start, int end) {
if (lazy[node] != 0) {
tree[node] += lazy[node] * (end - start + 1);
if (start != end) {
lazy[2*node] += lazy[node];
lazy[2*node+1] += lazy[node];
}
lazy[node] = 0;
}
}
void rangeUpdate(int node, int start, int end, int l, int r, int val) {
push(node, start, end);
if (start > r || end < l) return;
if (start >= l && end <= r) {
lazy[node] += val;
push(node, start, end);
return;
}
int mid = (start + end) / 2;
rangeUpdate(2*node, start, mid, l, r, val);
rangeUpdate(2*node+1, mid+1, end, l, r, val);
push(2*node, start, mid);
push(2*node+1, mid+1, end);
tree[node] = tree[2*node] + tree[2*node+1];
}
};📝 時間複雜度分析
| 操作 | 時間複雜度 | 空間複雜度 |
|---|---|---|
| 建構 | (O(n)) | (O(4n)) |
| 單點更新 | (O(\log n)) | - |
| 區間查詢 | (O(\log n)) | - |
| 區間更新 | (O(\log n)) | - |
🎯 經典題目練習
LeetCode 相關題目
Codeforces 相關題目
💡 實用技巧
1. 記憶體優化
// 動態建立節點,節省記憶體
struct Node {
int val;
Node* left;
Node* right;
Node(int v = 0) : val(v), left(nullptr), right(nullptr) {}
};2. 泛型設計
template<typename T>
class SegmentTree {
private:
function<T(T, T)> combine;
T identity;
public:
SegmentTree(vector<T>& arr, function<T(T, T)> op, T id)
: combine(op), identity(id) {
// 建構邏輯
}
};🔍 總結
線段樹是處理區間查詢問題的強力工具,掌握它的關鍵在於:
- 理解分治思想:大區間拆分成小區間
- 熟練遞迴操作:建構、查詢、更新都是遞迴實現
- 靈活應用變形:根據需求修改合併操作
- 善用懶惰標記:處理區間更新問題
NOTE線段樹雖然強大,但不是萬能的。對於某些特殊問題,BIT(樹狀陣列)或其他資料結構可能更適合。
掌握線段樹後,你會發現很多看似困難的區間問題都變得簡單了!繼續練習,加油! 💪
相關文章推薦:
線段樹 (Segment Tree) 完全攻略
https://vic88-web.vercel.app/posts/segment_tree/