线段树

[转]线段树在程序设计中的应用

2005年第4期(总第78期)(转自:http://www.mscenter.edu.cn/blog/like/articles/6106.html)

 
 
林盛华
(韶关市第一中学,广东  韶关  512000)
 
[摘  要]文章主要介绍了线段树的定义及构造、线段树的动态数据结构和静态数据结构,以及线段树的基本操作。还结合具体实例,具体阐述了线段树在程序设计中的应用。
[关键词]线段树;动态数据结构;静态数据结构;线段树的基本操作
 
[中图分类号] TP3     [文献标识码]A  [文章编号]1008-1151(2005)04-
 
[收稿日期] 2005-
[作者简介] 林盛华(1973-),男,江西南康人,广东省韶关市第一中学一级教师,研究方向:计算机程序设计。
 
 
在中学信息学竞赛中,经常遇到与区间、图形的面积、周长等有关的问题。处理涉及这些知识的问题,并不需要依赖很深的数学知识,但要提高处理此类问题的效率却又十分困难,为了提高程序的效率,经常需要使用一种特殊的数据结构:线段树。一个线段是对应于一个区间的,因此线段树也叫区间树。
1 线段树的构造信息
1.1 线段树的定义
线段树是一棵二叉树,将一个区间划分为一个个[i,i+1]的单元区间,每个单元区间对应线段树中的一个叶子结点。每个结点用一个变量count来记录覆盖该结点的线段条数。
设根为[a,b]的线段树记为T(a,b),区间的长度 ba记为 L。递归定义T[ a, b]:
L>1 :[ a, ( a+ b) div 2]为 T的左儿子;
[( a+ b) div 2, b]为T的右儿子。
L=1 :T为一个叶子节点。
区间[1,7]所对应的线段树如下图所示。区间上有一条线段[3,6]。
 
1.2 线段树的数据结构
1.2.1动态数据结构
如果采用动态的数据结构来实现线段树,结点的构造可以用如下数据结构:

Type
       Tnode=^Treenode;
       Treenode=record
                 B,E:integer;
                    Count:integer;
                    Lchild,Rchild:Tnode;
                End;
 
 

 
 
 

    其中B和E表示了该区间为[B,E];Count为一个计数器,通常记录覆盖到此区间的线段的个数。Lchild和Rchild分别是左右子树的根。
  1.2.2静态的数据结构
    有时为了方便,我们也采用静态的数据结构—完全二叉树。
Type
    Treenode=record
              b,e:integer;
              Cover:integer;
             End;
Var
   Tree:array[1..n] of treenode;
 
 
    前面所讲的内容都只是线段树的基本结构。通常利用线段树的时候需要在每个结点上增加一些特殊的数据域,并且它们是随线段的插入删除进行动态维护的。
2 线段树的基本操作
为了叙述的方便,这里以完全二叉树为例,说明线段树的基本操作。
2 .1 线段树的插入算法
procedure Insert(p, a, b: Integer);
var
   m: Integer;
begin
    if Tree[p].cover = 0 then{此区间未被完全覆盖}
    begin
        m := (Tree[p].b + Tree[p].e) div 2;{取中值}
        if (a = Tree[p].b) and (b = Tree[p].e) then {完全覆盖}
           Tree[p].cover := 1
        else if b <= m then Insert(p * 2, a, b){在左边}
        else if a >= m then Insert(p * 2 + 1, a, b){在右边}
        else begin
            Insert(p * 2, a, m);  {二分}
            Insert(p * 2 + 1, m, b);
          end;
    end;
end;
对此算法的解释:如果[a,b]完全覆盖了当前线段,那么显然该结点上的基数(即覆盖线段数)为1(作标记符号)。否则,如果[a,b]不跨越区间中点,就只对左树或者右树上进行插入。否则,在左树和右树上都要进行插入。注意观察插入的路径,一条待插入区间在某一个结点上进行“跨越”,此后两条子树上都要向下插入,但是这种跨越不可能多次发生。插入、删除区间的时间复杂度均为O(logn)。
2.2 线段树的统计算法
我们先给出一个概念:结点的测度。结点的测度m指的是结点所表示区间中线段覆盖过的长度。
比如,区间[1,7]所对应的线段树如下图所示,区间上有一条线段[3,6]。

结点的测度m如下式计算:
 
根据此式,我们很容易地算出上述线段树各结点的测度。
先算出叶子结点的测度:
COUNT[1,2]=0,COUNT[2,3]=0,COUNT[3,4]=1,CONUT[4,5]=1,COUNT[5,6]=1,COUNT[6,7]=0。接着依次算出内部结点的测度:
CONUT[2,4]=COUNT[2,3]+COUNT[3,4]=0+1=1
CONUT[1,4]=COUNT[1,2]+COUNT[2,4]=0+1=1
COUNT[5,7]=COUNT[5,6]+COUNT[6,7]=1+0=1
COUNT[4,7]=COUNT[4,5]+COUNT[5,7]=1+1=2
COUNT[1,7]=COUNT[1,4]+COUNT[4,7]=1+2=3
 
计算叶子结点的测度算法如下:
function Count(p: Integer): Integer;
begin
    if Tree[p].cover = 1 then {被完全覆盖}
       Count := Tree[p].e – Tree[p].b
    else if Tree[p].e – Tree[p].b = 1 then Count := 0{是单位区间}
    else Count := Count(p * 2) + Count(p * 2 + 1);{递归求解}
end;
 
3 线段树的应用
3.1 盒子问题
在一个1000米长的桌子上放着很多盒子,桌子的后方有一堵墙,如下图所示。假设人站得足够远,问:从桌子前方可以看到多少个盒子?
                 WALL
  [分析]我们可以把上述转换成这样的一道题:x轴上有若干条不同线段,将它们依次染上不同的颜色,问最后能看到多少种不同的颜色?(后染的颜色会覆盖原先的颜色)
我们可以这样规定:x轴初始是颜色0,第一条线段染颜色1,第二条线段染颜色2,以此类推。
原先构造线段树的方法不再适用,但是我们可以通过修改线段树的cover域的定义,使得这道题也能用线段树来解。
定义cover如下:cover=-1表示该区间由多种颜色组成。cover>=0表示该区间只有一种单一的颜色cover。
[插入算法如下]

procedure Insert(p, l, r, a, b, c: Integer);
var  m: Integer;
begin
    if Tree[p].cover <> c then
    begin
        m := (l + r) div 2;
        if (a = l) and (b = r) then Tree[p].cover := c
        else begin
            if Tree[p].cover >= 0 then
            begin
                Tree[p * 2].cover := Tree[p].cover;
Tree[p * 2 + 1].cover := Tree[p].cover;
                Tree[p].cover := -1;
            end;
            if b <= m then Insert(p * 2, l, m, a, b, c)
            else if a >= m then  Insert(p * 2 + 1, m, r, a, b, c)
            else begin
                Insert(p * 2, l, m, a, m, c);
                Insert(p * 2 + 1, m, r, m, b, c);
            end;
        end;
•             end;
•         end;
 
 

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

 
 
 
 
 
[统计算法如下]
使用一个数组Flag,初始化为0。遍历线段树,对于每种颜色c对Flag[c]赋值1。最后统计Flag中1的个数即可。(注意颜色0应该排除在外,可以在最后减1)

procedure Count(p, l, r: Integer);
begin
    if Tree[p].cover >= 0 then Flag[Tree[p].cover] := 1
    else if r – l > 1 then
    begin
        Count(p * 2, l, (l + r) div 2);
        Count(p * 2 + 1, (l + r) div 2, r);
    end;
end;
 
 

 
 
 
 
 
 
 
 
 
 
 

3.2 染色问题
我们把上题略加改动:x轴上有若干条不同线段,将它们依次染上颜色(可以相同),连续的相同颜色被视作一段。问x轴被分成多少段?
[分析]我们仍然能够利用类似上题的做法,利用线段树解决。我们需要修改线段树域的定义:
原先的Cover域取消,增设两个域:lcolor、rcolor,分别表示该区间两个端点的颜色。颜色种类分别用0-n的整数表示,0表示底色。插入算法稍作变化,记录下每个结点的两个端点的颜色种类。而在统计结点的值时,如果该结点的左孩子的右端点的颜色与右孩子的左端点颜色相同,则结点的值则为:左孩子的值+右孩子的值-1。
假设用m表示该区间的线段数,则计算式如下:
  程序略。
线段树的构造利用了二分法的思想,在具体的解题中,对域的设置是相当灵活的,并且随着线段的插入进行动态地维护。这是线段树解题的关键、是解题的灵魂。
 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值