HDU1166敵兵佈陣(線段樹入門,單點更新區間查詢)

前言

線段樹是用來維護一段區間某種操作的樹形數據結構,由於設計到區間,成員節點中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;
}

總結

相比樹狀數組,線段樹更容易理解,不過代碼也比較難寫,但是處理的問題範圍比較大,但是常數也比較大。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章