1. 程式人生 > >線段樹的實現及其經典用法(C++實現)

線段樹的實現及其經典用法(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 結點定義:

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.2 建立線段樹
//建立二叉樹
//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;
}
     2.3在一棵線段樹裡插入一條線段
//插入一條線段
//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