HDU1166_最簡單易學的線段樹
題目連結題目大意:有N個工兵的營地,對於這些營地有三種操作。
(1) Add i j,i和j為正整數,表示第i個營地增加j個人(j不超過30)
(2)Sub i j ,i和j為正整數,表示第i個營地減少j個人(j不超過30);
(3)Query i j ,i和j為正整數,i<=j,表示詢問第i到第j個營地的總人數;
(4)End 表示結束,這條命令在每組資料最後出現;
要求快速的實現這三種操作。首先我們可以分析到的是,三種操作可以被歸併為兩種,也就是修改和查詢。每個營地相當於一個點,也就是點修改,查詢i到j營地,就是所謂的區間查詢。點修改區間查詢,查詢的是區間的和。由於具有區間的可加性,所以我們採用線段樹這種資料結構來維護。至於什麼是線段樹?線段樹是一顆二叉樹,每個非葉子節點都會有兩個兒子,每個節點儲存一區間的資訊。線段樹用到的是一種二分的思想,我們考慮一個數組。有n個元素,現在就來考慮這n個元素的和。我們將這些元素放在一顆二叉樹上,怎麼放?二分,不明白?OK,上圖
。
我們用1-6生成一顆線段樹。首先需要明確的是,節點裡面存的值很重要,這個值就是需要維護的值。由於本題我們需要求和所有的元素,所以這裡維護的就是區間和。根節點首先放置的就是1-6的和,也就是21,然後左兒子是(1+6)/2,所以左邊就是1-3,節點儲存的值就是6,有兒子就是4-6.也就是15,繼續二分下去,那麼我們可以將每一個元素更新到葉子節點上去。那麼這個資料有什麼好處呢,首先肯定是方便我們查詢區間和,我們其實做了一個類似於預處理的操作,但是,這個不是簡單的預處理,由於詢問的區間並不能事先知道,所以你不能預處理掉所有的可能區間,但是我們這樣的預處理,也就誰用一棵樹來儲存區間和,可以證明,所有的區間和都是可以通過這些樹上的一些區間和累加起來。這也就是為什麼大大的加速了查詢的操作,再來看修改,修改一個區間,我們可以發現,其實任意一個元素都是在每一層裡出現一次,也就是說我們修改的時候只需要每一層的這個元素所在的區間,可以證明這個操作實在log2(n)之內是可以完成的。所以點修改也是十分的迅速的。
在上邊的分析中,其實我們隱含的已經使用了一些條件,而這些條件也正是使用線段樹的一些顯著的特徵:符合區間的加法,大量的修改查詢操作等。回到剛剛的題,其實我們使用線段樹可以輕易的解決掉。下邊給出程式碼,對於關鍵的地方給出解釋。
#include <iostream> #include<string> using namespace std; const int maxn = 50010; int segTree[maxn * 4]; //更新父節點操作,由於我們是使用的陣列模擬的樹,所以根節點和左右子樹下標的關係是: //根:k,左子樹:2*k,右子樹:2*k+1, //使用位運算也就是:左:k<<1,右:k<<1|1. void pushUp(int root) { segTree[root] = segTree[root * 2] + segTree[root * 2 + 1]; } void build(int root, int left, int right) { if (left == right) { cin >> segTree[root]; return; } int mid = (left + right) / 2; //遞迴建立左子樹和右子樹 build(root * 2, left, mid); build(root * 2 + 1, mid + 1, right); //每次更新父節點值. //很多初學者不是很明白這裡為什麼可以更新父節點,其實這裡是遞迴的一個本質,遞迴是基於棧完成 //的,所以當前操作完成後會返回到上一層,並且執行這個更新的操作,也就更新了父節點 pushUp(root); } void update(int root, int p, int add, int left, int right) //單節點更新,p為待更新節點下標,add為需增加或減少的值 { if (left == right) //找到單節點就更新 { segTree[root] += add; return; } //二分查詢指定節點 int mid = (left + right) / 2; if (p <= mid) { update(root * 2, p, add, left, mid); } else { update(root * 2 + 1, p, add, mid + 1, right); } pushUp(root); } int query(int root, int q_left, int q_right, int now_left, int now_right) //查詢區間 { if (q_left <= now_left && q_right >= now_right) //當前節點區間包含在查詢區間內 { return segTree[root]; } int mid = (now_left + now_right) / 2; int sum = 0; if (q_left <= mid) { sum += query(root * 2, q_left, q_right, now_left, mid); } if (q_right > mid) { sum += query(root * 2 + 1, q_left, q_right, mid + 1, now_right); } return sum; } int main() { ios::sync_with_stdio(false); int t, n; string op; cin >> t; for (int i = 1; i <= t; i++) { cout << "Case " << i << ":" << endl; cin >> n; build(1, 1, n); while (cin >> op) { if (op[0] == 'E') { break; } int a, b; cin >> a >> b; if (op[0] == 'Q') //詢問 { int ans = query(1, a, b, 1, n); cout << ans << endl; } else if (op[0] == 'S') //減 { update(1, a, -b, 1, n); } else if (op[0] == 'A') //加 { update(1, a, b, 1, n); } } } return 0; }