線段樹在一些acm題目中經常見到,這種數據結構主要應用在計算幾何和地理信息系統中。下圖就為一個線段樹:
(PS:可能你見過線段樹的不同表示方式,但是都大同小異,根據自己的需要來建就行。)
1.線段樹基本性質和操作
線段樹是一棵二叉樹,記為T(a, b),參數a,b表示區間[a,b],其中b-a稱為區間的長度,記為L。
線段樹T(a,b)也可遞歸定義為:
若L>1 : [a, (a+b) div 2]為 T的左兒子; [(a+b) div 2,b]為T 的右兒子。 若L=1 : T為葉子節點。
線段樹中的結點一般采取如下數據結構:
struct Node { int left,right; //區間左右值 Node *leftchild; Node *rightchild; };
線段樹的建立:
Node *build(int l , int r ) //建立二叉樹 { Node *root = new Node; root->left = l; root->right = r; //設置結點區間 root->leftchild = NULL; root->rightchild = NULL; if ( l +1< r ) { int mid = (r+l) >>1; root->leftchild = build ( l , mid ) ; root->rightchild = build ( mid , r) ; } return root; }
線段樹中的線段插入和刪除:
增加一個cover的域來計算一條線段被覆蓋的次數,因此在建立二叉樹的時候應順便把cover置0。
插入一條線段[c,d]:
void Insert(int c, int d , Node *root ) { if(c<= root->left&&d>= root->right) root-> cover++; else { if(c < (root->left+ root->right)/2 ) Insert (c,d, root->leftchild ); if(d > (root->left+ root->right)/2 ) Insert (c,d, root->rightchild ); } }
刪除一條線段[c,d]:
void Delete (int c , int d , Node *root ) { if(c<= root->left&&d>= root->right) root-> cover= root-> cover-1; else { if(c < (root->left+ root->right)/2 ) Delete ( c,d, root->leftchild ); if(d > (root->left+ root->right)/2 ) Delete ( c,d, root->rightchild ); } }
2.線段樹的運用
線段樹的每個節點上往往都增加了一些其他的域。在這些域中保存了某種動態維護的信息,視不同情況而定。這些域使得線段樹具有極大的靈活性,可以適應不同的需求。
例一:
桌子上零散地放着若干個盒子,桌子的后方是一堵牆。如圖所示。現在從桌子的前方射來一束平行光, 把盒子的影子投射到了牆上。問影子的總寬度是多少?
這道題目是一個經典的模型。在這里,我們略去某些處理的步驟,直接分析重點問題,可以把題目抽象地描述如下:x軸上有若干條線段,求線段覆蓋的總長度,即S1+S2的長度。
2.1最直接的做法:
設線段坐標范圍為[min,max]。使用一個下標范圍為[min,max-1]的一維數組,其中數組的第i個元素表示[i,i+1]的區間。數組元素初始化全部為0。對於每一條區間為[a,b]的線段,將[a,b]內所有對應的數組元素均設為1。最后統計數組中1的個數即可。
初始 0 0 0 0 0 [1,2] 1 0 0 0 0 [3,5] 1 0 1 1 0 [4,6] 1 0 1 1 1 [5,6] 1 0 1 1 1
其缺點是時間復雜度決定於下標范圍的平方,當下標范圍很大時([0,10000]),此方法效率太低。
2.2離散化的做法:
基本思想:先把所有端點坐標從小到大排序,將坐標值與其序號一一對應。這樣便可以將原先的坐標值轉化為序號后,對其應用前一種算法,再將最后結果轉化回來得解。該方法對於線段數相對較少的情況有效。
示例:
[10000,22000] [30300,55000] [44000,60000] [55000,60000]
排序得10000,22000,30300,44000,55000,60000
對應得1, 2, 3, 4, 5, 6
然后是 [1,2] [3,5] [4,6] [5,6]
初始 0 0 0 0 0 [1,2] 1 0 0 0 0 [3,5] 1 0 1 1 0 [4,6] 1 0 1 1 1 [5,6] 1 0 1 1 1
10000,22000,30300,44000,55000,60000
1, 2, 3, 4, 5, 6
(22000-10000)+(60000-30300)=41700
此方法的時間復雜度決定於線段數的平方,對於線段數較多的情況此方法效率太低。
2.3使用線段樹的做法:
給線段樹每個節點增加一個域cover。cover=1表示該結點所對應的區間被完全覆蓋,cover=0表示該結點所對應的區間未被完全覆蓋。
如下圖的線段樹,添加線段[1,2][3,5][4,6]
插入算法:
void Insert(Node *root , int a , int b) { int m; if( root ->cover == 0) { m = (root->left+ root->right)/2 ; if (a == root->left && b == root->right) root ->cover =1; else if (b <= m) Insert(root->leftchild , a, b); else if (a >= m) Insert(root->rightchild , a, b); else { Insert(root->leftchild ,a, m); Insert(root->rightchild , m, b); } } }
統計算法:
int Count(Node *root) { int m,n; if (root->cover == 1) return (root-> right - root-> left); else if (root-> right - root-> left== 1 )return 0; m= Count(root->leftchild); n= Count(root->rightchild); return m+n; }