線段樹的實現及其經典用法(C++實現)
一、線段樹的定義
首先,線段樹是一棵完全二叉樹。它的特點是:每個結點表示的是一個線段,或者說是一個區間。事實上,一棵線段樹的根結點表示的是“整體”區間,而它的左右子樹也是一棵線段樹,分別表示區間的左半邊和右半邊。樹中的每個結點表示一個區間[a,b]。每一個葉子結點表示一個單位區間。對於每一個非葉結點所表示的結點[a,b],其左孩子表示的區間為[a,(a+b)/2],右孩子表示的區間為[(a+b)/2,b]。 用T(a, b)表示一棵線段樹,引數a,b表示區間[a,b],其中b-a稱為區間的長度,記為L。
線段樹T(a,b)也可遞迴定義為:
若L>1 : [a, (a+b)/ 2]為T的左孩子; [(a+b) / 2,b]為T的右孩子。 若L=1 : T為葉子節點。
下圖就是一棵線段樹:
二、線段樹的實現(C++)
2.1 結點定義:
2.2 建立線段樹class regionTreeNode { public: int left; //左端點值 int right; //右端點值 int cover; //被覆蓋的次數 regionTreeNode *leftChild; //指向左孩子的指標 regionTreeNode *rightChild; //指向右孩子的指標 regionTreeNode(): left(0), right(0), cover(0), leftChild(NULL), rightChild(NULL){} //建構函式 };
2.3在一棵線段樹裡插入一條線段//建立二叉樹 //min和max分別表示線段的左端點和右端點 regionTreeNode* createRegionTree(int min, int max) { if (max - min < 1) { cout<<"輸入的引數不合法!"; return NULL; } regionTreeNode *rootNode = new regionTreeNode(); rootNode->left = min; rootNode->right = max; rootNode->cover = 0; rootNode->leftChild = NULL; rootNode->rightChild = NULL; //葉子結點 if (max - min == 1) { return rootNode; } else if (max - min > 1) { int mid = (min + max) >> 1; rootNode->leftChild = createRegionTree(min, mid); //遞迴構造左子樹 rootNode->rightChild = createRegionTree(mid, max); //遞迴構造右子樹 } return rootNode; }
//插入一條線段
//a:插入線段的左端點
//b:插入線段的右端點
//tree:插入線段樹的根結點
void insertRegion(int a, int b, regionTreeNode *tree)
{
if (tree == NULL || a < tree->left || b > tree->right)
{
cout<<"輸入的引數不合法!";
return;
}
if (tree->left == a && tree->right == b)
{
tree->cover++;
return;
}
int mid = (tree->left + tree->right) >> 1;
if (b <= mid)
{
insertRegion(a, b, tree->leftChild);
}
else if (a >= mid)
{
insertRegion(a, b, tree->rightChild);
}
else
{
insertRegion(a, mid, tree->leftChild);
insertRegion(mid, b, tree->rightChild);
}
}
2.4在一棵線段樹裡刪除一條線段
//刪除一條線段
//c:刪除線段的左端點
//d:刪除線段的右端點
//tree:刪除線段樹的根結點
void deleteRegion(int c, int d, regionTreeNode *tree)
{
if (tree == NULL || c < tree->left || d > tree->right)
{
cout<<"輸入的引數不合法!";
return;
}
if (c == tree->left && d == tree->right)
{
tree->cover--;
return;
}
int mid = (tree->left + tree->right) >> 1;
if (d <= mid)
{
deleteRegion(c, d, tree->leftChild);
}
else if (c >= mid)
{
deleteRegion(c, d, tree->rightChild);
}
else
{
deleteRegion(c, mid, tree->leftChild);
deleteRegion(mid, d, tree->rightChild);
}
}
三、線段樹的典型應用
題目:桌子上零散地放著若干個盒子,桌子的後方是一堵牆。現在從桌子的前方射來一束平行光, 把盒子的影子投射到了牆上。問影子的總寬度是多少?
這道題目是一個經典的模型。在這裡,我們可以把題目抽象地描述如下:x軸上有若干條線段,求線段覆蓋的總長度。
用線段樹來求解:
給線段樹每個節點增加一個域cover。cover=1表示該結點所對應的區間被完全覆蓋,cover=0表示該結點所對應的區間未被完全覆蓋。
程式碼如下:
struct SegmentTreeNode //首先定義一個線段樹節點的資料結構:
{
int a; //線段起點
int b; //線段終點
bool cover; //是否被全部覆蓋
int color; //線段的顏色
SegmentTreeNode *left;
SegmentTreeNode *right;
};
我們首先在給定的線段區間上去構建一顆完全二叉樹,作為我們線段樹的一棵基樹
SegmentTreeNode* createTree(int m, int n) //構建一棵完全二叉樹,作為線段樹的基樹
{
SegmentTreeNode *root = new SegmentTreeNode();
root->a = m;
root->b = n;
root->cover = false;
root->color = 0;
root->left == NULL;
root->right == NULL;
if((n - m) == 1)
{
return root;
}
else if(n - m > 1)
{
int mid =(m + n) >> 1;
root->left = createTree(m, mid);
root->right = createTree(mid, n);
}
return root;
}
然後我們將用到的線段插入到基樹當中,去更新基樹的cover域:
void Insert(SegmentTreeNode *root, int m, int n) //求解覆蓋總長度的問題
{
if(root->cover == true || root == NULL)
{
return;
}
int mid = (root->a + root->b) >> 1;
if(m == root->a && n == root->b)
{
root->cover = true;
}
else if(m >= mid)
{
Insert(root->right, m, n);
}
else if(n <= mid)
{
Insert(root->left, m, n);
}
else
{
Insert(root->left, m, mid);
Insert(root->right, mid, n);
}
}
統計線段樹中節點cover域為true的線段的總長度,並返回結果,問題得解:
int Count(SegmentTreeNode *root)
{
if(root->cover == true)
{
return root->b - root->a;
}
else
{
if(root->b - root->a == 1)
{
return 0;
}
else
{
return Count(root->left) + Count(root->right);
}
}
}
把上述問題稍作變換,就可以得到另一個問題2:
桌子上零散地放著若干個盒子,桌子的後方是一堵牆。問從桌子前方可以看到多少個盒子?假設人站得足夠遠。
分析一下可知:
可以這樣來看這道題:x軸上有若干條不同線段,將它們依次染上不同的顏色,問最後能看到多少種不同的顏色?(後染的顏色會覆蓋原先的顏色)
我們可以這樣規定:x軸初始是顏色0,第一條線段染顏色1,第二條線段染顏色2,以此類推。
原先構造線段樹的方法不再適用,但是我們可以通過修改線段樹的cover域的定義,使得這道題也能用線段樹來解。
那麼我們來給線段樹新增一個color域,color = -1表示該區間存在多種顏色的線段,color = 0表示該區間未被著色,color > 0表示該區間僅存在一種顏色。
然後我們修改我們的insert方法和統計方法來求解這個問題:
void ColorInsert(SegmentTreeNode *root, int l, int r, int color) //求解染色段數的問題。
{
if(root->color != color || root->cover == false) //假如插入的線段跟當前線段顏色相同,我們檢查該線段是否被完全著色
{
if(root->b - root->a == 1)//假如線段區間長度為1,直接著色
{
root->color = color;
root->cover = true;
}
else
{
if(root->color == 0 || root->a == l && root->b == r)//如果區間未被著色,或者插入的線段能夠完全覆蓋區間,則區間直接著色
{
root->color = color;
if(root->a == l && root->b == r)
root->cover = true; //完全覆蓋區間,將cover置為true
}
else
{
if(root->color != color) //如果區間已經著色,並且cover=flase,則更新color = -1表示區間存在多種顏色。
{
root->color = -1;
}
}
int mid = (root->a + root->b) >> 1; //遞迴的給子樹著色。
if(l >= mid)
{
ColorInsert(root->right, l, r, color);
}
else if(r <= mid)
{
ColorInsert(root->left, l, r, color);
}
else
{
ColorInsert(root->right, mid, r, color);
ColorInsert(root->left, l, mid, color);
}
}
}
}
下面就是我們解決這個問題,用到的統計策略,當我們遇到區間的color大於0時,表示區間只有一種顏色,將區間的顏色值放入一個set,當color小於0,則遞迴的查詢,線段樹子樹並且判斷該顏色是否已經存在。最後返回set的size即可
void Color(SegmentTreeNode *root, set<int> &iset)
{
if(root->color == 0 || root == NULL)
{
return;
}
if(root->color > 0)
{
if(!iset.count(root->color))
{
iset.insert(root->color);
}
}
else
{
Color(root->left, iset);
Color(root->right, iset);
}
}
參考文章:http://www.cnblogs.com/shuaiwhu/archive/2012/04/22/2464583.html