線段樹的定義
首先,線段樹既是線段也是樹,並且是一棵二叉樹,每個結點是一條線段,每條線段的左右兒子線段分別是該線段的左半和右半區間,遞歸定義之後就是一棵線段樹,圖示如下
圖1.線段樹示意圖
定義線段樹的數據結構
struct Line{
int left, right, count;
Line *leftChild, *rightChild;
Line(int l, int r): left(l), right(r) {}
};
PS:其中的count字段表示該條線段有幾條
明白了線段樹的定義之後,我們來舉一個例子來說明線段樹的應用
例題:給定N條線段,{[2, 5], [4, 6], [0, 7]}, M個點{2, 4, 7},判斷每個點分別在幾條線段出現過
看到題目,有的人第一感覺想出來的算法便是對於每一個點,逐個遍歷每一條線段,很輕鬆地判斷出來每個點在幾條線段出現過,小學生都會的算法,時間複雜度爲O(M*N)
如國N非常大,比如2^32-1, M也非常大M = 2^32 - 1, O(M*N)的算法將是無法忍受的,這個時候,線段樹便隆重登場了
線段樹的解法:
1.首先,我們找出一個最大的區間能夠覆蓋所有的線段,遍歷所有的線段,找線段的最值(左端點的最小值,右端點的最大值)便可以確定這個區間,對於{[2, 5], [4, 6], [0, 7]}, 這個區間爲[0, 7],時間複雜度爲O(N)
2.然後,根據此區間建一棵線段樹(見圖1), 時間複雜度爲O(log(MAX-MIN))
3.對於每一條線段A,從根節點開始遍歷這棵線段樹,對於每一個當前遍歷的結點NODE(其實線段樹中每一個結點就是一條線段),考慮三種情況
a)如果線段A包含在線段NODE的左半區間,那麼從NODE的左兒子(其實就是NODE的左半區間)開始遍歷這棵樹
b)如果線段A包含在線段NODE的右半區間,那麼從NODE的右兒子(其實就是NODE的右半區間)開始遍歷這棵樹
c)如果線段A剛好和線段NODE重合,停止遍歷,並將NODE中的count字段加1
d)除了以上的情況,就將線段A的左半部分在NODE的左兒子處遍歷,將A的右半部分在NODE的右兒子處遍歷
補充說明:對於以上的步驟,所做的工作其實就是不斷地分割每一條線段,使得分割後的每一條小線段剛好能夠落在線段樹上,舉個例子,比如要分割[2, 5],首先將[2, 5]和[0, 7]比較,符合情況d, 將A分成[2, 3]與[4, 5]
I)對於[2, 3]從[0, 7]的左半區間[0, 3]開始遍歷
將[2, 3]與[0, 3]比較,滿足情況b,那麼從[0, 3]的右半區間[2, 3]開始遍歷,發現剛好重合,便將結點[2, 3]count字段加1
II)對於[4, 5]從[0, 7]的右半區間[4, 7]開始遍歷
將[4, 5]與[4, 7]比較,滿足情況b,從[4, 7]的左半區間[4, 5]開始遍歷,發現剛好重合,便將結點[4, 5]count字段加1
於是對於[2, 5]分割之後線段樹的情況爲圖2
圖2.分割[2,5]之後線段樹的情況
顯然,我們看到,上述的遍歷操作起始就是將[2, 5]按照線段樹中的線段來分割,分割後的[2, 3]與[4, 5]其實是與[2, 5]完全等效的
最後,我們將剩下的兩條線段按照同樣的步驟進行分割之後,線段樹的情況如下圖3
這一步的時間複雜度爲 O(N*log(MAX-MIN))
4.最後,對於每一個值我們就可以開始遍歷這一顆線段樹,加上對於結點的count字段便是在線段中出現的次數
比如對於4,首先遍歷[0, 7],次數 = 0+1=1;4在右半區間,遍歷[4, 7],次數 = 1+0=0;4在[4, 7]左半區間, 次數 = 1+2=3;4在[4, 5]左半區間,次數 = 3+0 = 4,遍歷結束,次數 = 3說明4在三條線段中出現過,同理可求其他的值,這一步的時間複雜度爲O(M*log(MAX-MIN))
最後,總的時間複雜度爲O(N)+O(log(MAX-MIN))+O(N*log(MAX-MIN))+(M*log(MAX-MIN)) = O((M+N)*log(MAX-MIN))
由於log(MAX-MIX)<=64所以最後的時間複雜度爲O(M+N)
最後,放出源碼
#include <iostream>
using namespace std;
struct Line{
int left, right, count;
Line *leftChild, *rightChild;
Line(int l, int r): left(l), right(r) {}
};
//建立一棵空線段樹
void createTree(Line *root) {
int left = root->left;
int right = root->right;
if (left < right) {
int mid = (left + right) / 2;
Line *lc = new Line(left, mid);
Line *rc = new Line(mid + 1, right);
root->leftChild = lc;
root->rightChild = rc;
createTree(lc);
createTree(rc);
}
}
//將線段[l, r]分割
void insertLine(Line *root, int l, int r) {
cout << l << " " << r << endl;
cout << root->left << " " << root->right << endl << endl;
if (l == root->left && r == root->right) {
root->count += 1;
} else if (l <= r) {
int rmid = (root->left + root->right) / 2;
if (r <= rmid) {
insertLine(root->leftChild, l, r);
} else if (l >= rmid + 1) {
insertLine(root->rightChild, l, r);
} else {
int mid = (l + r) / 2;
insertLine(root->leftChild, l, mid);
insertLine(root->rightChild, mid + 1, r);
}
}
}
//樹的中序遍歷(測試用)
void inOrder(Line* root) {
if (root != NULL) {
inOrder(root->leftChild);
printf("[%d, %d], %d\n", root->left, root->right, root->count);
inOrder(root->rightChild);
}
}
//獲取值n在線段上出現的次數
int getCount(Line* root, int n) {
int c = 0;
if (root->left <= n&&n <= root->right)
c += root->count;
if (root->left == root->right)
return c;
int mid = (root->left + root->right) / 2;
if (n <= mid)
c += getCount(root->leftChild, n);
else
c += getCount(root->rightChild, n);
return c;
}
int main() {
int l[3] = {2, 4, 0};
int r[3] = {5, 6, 7};
int MIN = l[0];
int MAX = r[0];
for (int i = 1; i < 3; ++i) {
if (MIN > l[i]) MIN = l[i];
if (MAX < r[i]) MAX = r[i];
}
Line *root = new Line(MIN, MAX);
createTree(root);
for (int i = 0; i < 3; ++i) {
insertLine(root, l[i], r[i]);
}
inOrder(root);
int N;
while (cin >> N) {
cout << getCount(root, N) << endl;
}
return 0;
}