1. 程式人生 > >HDU1166_最簡單易學的線段樹

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;
}