線段樹(segment tree)


線段樹在一些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
[12]   1   0  0  0  0
[35]   1   0  1  1  0
[46]   1   0  1  1  1
[56]   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
[12]   1   0  0  0  0
[35]   1   0  1  1  0
[46]   1   0  1  1  1
[56]   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;
}

 

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM