前言
線段樹是用來維護一段區間某種操作的樹形數據結構,由於設計到區間,成員節點中l,r表示區間[l,r]。對於線段樹的構造實際上是利用了二分的思想,從而使操作降到log級。
數據結構
struct Node {
int l,r;
int sum;
};
Node LTree[maxn << 2];
表示每個節點維護[l,r]區間的operator(這裏的操作是區間和),由於二分每個節點都有左右孩子,規模大小差不多是三倍N大,這裏直接開4倍N。(這其實也可以動態分配內存,但是靜態的簡潔好寫,減少BUG率)
建樹
void Build (int l, int r, int idx) { //建立
LTree[idx].l = l;
LTree[idx].r = r;
if (l == r) { //到達葉子節點
LTree[idx].sum = val[l];
return ;
}
int mid = l + ((r - l) >> 1);
Build(l, mid, idx << 1); //遞歸建立左子樹
Build(mid + 1, r, idx << 1 | 1); //遞歸建立右子樹
LTree[idx].sum = LTree[idx << 1].sum + LTree[idx << 1 | 1].sum; //左右孩子值更新完畢後把和傳上來(PushUp)
}
後序遍歷所有二分的區間,最後有個PushUp操作,其實就是把子區間的結果向上更新。
更新
void Update (int k, int num, int idx) { //更新
// LTree[idx].sum += num;
if (LTree[idx].l == k && LTree[idx].r == k) { //更新到了葉子節點
LTree[idx].sum += num;
return ;
}else if ( k <= (LTree[idx].l + ((LTree[idx].r - LTree[idx].l) >> 1)) ) { //k位置含於左區間
Update(k, num, idx << 1);
}else { //k位置含於右區間
Update(k, num, idx << 1 | 1);
}
LTree[idx].sum = LTree[idx << 1].sum + LTree[idx << 1 | 1].sum;
}
更新這裏可以先序更新也可以後序更新區間操作。
- 先序更新含義:
我要在單點更新數字,先把包含該點的區間更新完畢後再找那個還包含該點的子區間更新。 - 後序更新含義:
我要在單點更新數字,先把包含該點的子區間更新完畢後,再更加子區間的所有情況向上更新母區間。
查詢
int Query (int l, int r, int idx) { //查詢
if (l <= LTree[idx].l && r >= LTree[idx].r) { //查詢的區間包含當前節點區間,這個節點的和全要
return LTree[idx].sum;
}else {
int mid = LTree[idx].l + ((LTree[idx].r - LTree[idx].l) >> 1);
if (r <= mid) { //完全在左區間中
return Query(l, r, idx << 1);
}else if (l > mid) { //完全在右區間中
return Query(l, r, idx << 1 | 1);
}else { //左右區間各佔一部分
return Query(l, mid, idx << 1) + Query(mid + 1, r, idx << 1 | 1);
}
}
}
這裏很容易寫錯的地方是查詢的是[l,r]包含的子區間而不是哪些子區間包含[l,r]。比如當我發現r<=mid後很容易就寫成Query(l, mid, idx << 1)。而正確的就類似於把要查詢的區間分成m個子區間,分別從這些子區間得到貢獻反饋過來。所以遞歸的出口就是:當發現節點的區間被[l,r]完全包含時我就把這個值取出來,即完成了這一小部分。
HDU1166–AC代碼
#include <iostream>
#include <string>
using namespace std;
typedef long long LL;
const int maxn = (int)5e4+5;
struct Node {
int l,r;
int sum;
};
int val[maxn],n;
Node LTree[maxn << 2];
void Build (int l, int r, int idx) { //建立
LTree[idx].l = l;
LTree[idx].r = r;
if (l == r) { //到達葉子節點
LTree[idx].sum = val[l];
return ;
}
int mid = l + ((r - l) >> 1);
Build(l, mid, idx << 1); //遞歸建立左子樹
Build(mid + 1, r, idx << 1 | 1); //遞歸建立右子樹
LTree[idx].sum = LTree[idx << 1].sum + LTree[idx << 1 | 1].sum; //左右孩子值更新完畢後把和傳上來(PushUp)
}
void Update (int k, int num, int idx) { //更新
// LTree[idx].sum += num;
if (LTree[idx].l == k && LTree[idx].r == k) { //更新到了葉子節點
LTree[idx].sum += num;
return ;
}else if ( k <= (LTree[idx].l + ((LTree[idx].r - LTree[idx].l) >> 1)) ) { //k位置含於左區間
Update(k, num, idx << 1);
}else { //k位置含於右區間
Update(k, num, idx << 1 | 1);
}
LTree[idx].sum = LTree[idx << 1].sum + LTree[idx << 1 | 1].sum;
}
int Query (int l, int r, int idx) { //查詢
if (l <= LTree[idx].l && r >= LTree[idx].r) { //查詢的區間包含當前節點區間,這個節點的和全要
return LTree[idx].sum;
}else {
int mid = LTree[idx].l + ((LTree[idx].r - LTree[idx].l) >> 1);
if (r <= mid) { //完全在左區間中
return Query(l, r, idx << 1);
}else if (l > mid) { //完全在右區間中
return Query(l, r, idx << 1 | 1);
}else { //左右區間各佔一部分
return Query(l, mid, idx << 1) + Query(mid + 1, r, idx << 1 | 1);
}
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
int T;
cin >> T;
for (int k = 1; k <= T; k++) {
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> val[i];
}
Build(1, n, 1); //建立線段樹
string command;
int i,j;
cout << "Case " << k << ":\n";
while (cin >> command && command.at(0) != 'E') {
cin >> i >> j;
switch (command.at(0)) {
case 'A':
Update(i, j, 1);
break;
case 'S':
Update(i, -j, 1);
break;
case 'Q':
cout << Query(i, j, 1) << '\n';
break;
default:
break;
}
}
}
return 0;
}
總結
相比樹狀數組,線段樹更容易理解,不過代碼也比較難寫,但是處理的問題範圍比較大,但是常數也比較大。