1. 程式人生 > 其它 >OI 學習筆記 (2):線段樹

OI 學習筆記 (2):線段樹

前言

本文淺談了線段樹的幾種模型和一些例題。

參考資料

概況

線段樹,是一類可以在對數複雜度內處理區間型問題的資料結構。

這是一張線段樹的圖。容易發現,線段樹是一棵二叉樹,每個節點儲存了這個節點管理的區間 \(l,r\)(黑色數字)這個區間的 \(\operatorname{sum}\)(紅色數字)(也可以是 \(\max,\min\) 等等)。

構造

在圖中,我們發現了線段樹的一些特徵:一個節點的兩個兒子節點分別管理這個節點的一半,一個節點的 \(\operatorname{sum}\) 值等於兩個兒子節點的 \(\operatorname{sum}\) 值。通過這兩個特徵,就可以通過遞迴的方式構造一棵線段樹(應該都知道二叉樹的簡便構造方法吧)

CI N = 1e5, N4 = 4e5; int sum[N4 + 5], l[N4 + 5], r[N4 + 5], arr[N + 5]; // 線段樹需要開四倍大小
void pushup (int root) {sum[root] = sum[root << 1] + sum[root << 1 | 1];} // 線段樹子節點向父親節點更新
void build (int root, int L, int R) {
	l[root] = L; r[root] = R;
	if (L == R) {sum[root] = arr[L]; return ;} // 葉子節點
	int mid = (L + R) >> 1; build (root << 1, L, mid); build (root << 1 | 1, mid + 1, R);
	pushup (root); // 向上更新
}

區間查詢+單點修改

區間查詢

還是上面那棵線段樹,如果我們需要查詢區間 \([1,7]\) 的和,一種做法當然是從 \([1,1]\) 加到 \([7,7]\)(那你寫線段樹幹嘛)。但是我們發現,從 \([1,1]\)\([4,4]\) 的和就是 \([1,4]\)\(\operatorname{sum}\) 值。所以 \([1,7]\) 的和就等於 \([1,4],[5,6],[7,7]\) 的和。但是怎麼判斷查詢區間可以分成幾個小區間呢?考慮分類,我們從根節點向下遞迴(因為從上向下遞迴的,可以保證分割的區間數最少):如果當前區間包含在查詢區間內,那麼直接返回該節點 \(\operatorname{sum}\)

值;如果查詢區間與當前區間的左兒子有交集,向左兒子遞迴;如果右兒子有交集,向右兒子遞迴。

int Sum (int root, int L, int R) {
	if (l[root] >= L && r[root] <= R) return sum[root]; // 查詢區間包含當前區間 
	if (r[root] < L || l[root] > R) return 0; // 無交集 
	int mid = (l[root] + r[root]) >> 1, s = 0;
	if (L <= mid) s += Sum (root << 1, L, R); // 左兒子有交集 
	if (R > mid) s += Sum (root << 1 | 1, L, R); // 右兒子有交集 
	return s;
}

單點修改

(比區間查詢簡單多了)只需要找到對應的葉子節點,在遞迴返回的時候向上更新就好了。

void change (int root, int x, int k) {
	if (l[root] == r[root]) {sum[root] += k; return ;} // 到達葉子節點
	if (l[root] > x || r[root] < x) return ;
	int mid = (l[root] + r[root]) >> 1;
	if (x <= mid) change (root << 1, x, k);
	else if (x > mid) change (root << 1 | 1, x, k);
	pushup (root); // 向上更新
}

單點查詢+區間修改

考慮換一種實現方式,我們構造一棵 \(\operatorname{sum}\) 值全為 \(0\) 的線段樹。區間修改時把包含的區間加上 \(k\) 的標記,單點查詢時把沿路的標記加起來,再加上這個點原本的值即可。