Picture问题的深入讨论
基于对数据结构选择的进一步分析,我们来重新考虑一下Picture问题的数据结构的选择,即采用树形结构来描述一组超元线段的状态。
一、线段树
受到累计扫描过程的启发,一组超元线段属于轮廓的数目,它与跨越该组超元线段的矩形的纵向边位置关系密切。不妨把矩形的纵向边投影到Y轴上,这样就把矩形的纵向边看作闭区间,并称之为闭区间Q。我们以“线段树”的树形数据结构来描述闭区间Q。作为工具,先简单研究线段树的特点。
线段树是描述单个或若干区间并的树形结构,属于平衡树的一种。使用线段树要求知道所描述的区间端点可能取到的值。换句话说,设A[1..N]是从小到大排列的区间端点集合,对于任意一个待描述的闭区间P=[x,y],存在1≤i≤j≤N使得x=a[i]且y=a[j],这里i, j称为x,y的编号。可以看到,即使是实数坐标,在线段树中也只有整数含义。以下所说的区间[x, y]如无特殊说明,x、y均是整数,即原始区间顶点坐标的编号。
线段树是一棵二叉树,将数轴划分成一系列的初等区间[I, I+1] (I=1—N-1)。每个初等区间对应于线段树的一个叶结点。线段树的内部结点对应于形如[ I, J ](J – I > 1)的一般区间。一般情况下,线段树的结点类型定义如下:
Type
Lines_Tree = Object
i, j : integer; {结点表示的区间的顶点标号I, J}
count : integer; {覆盖这一结点的区间数}
leftchild, rightchild : ↑Lines_Tree; {二叉树的两个子结点}
end
关于Lines_Tree的其它数据域与定义的运算将陆续添加。图8是一棵线段树,描述的区间端点可以有10种取值。其中记录着一个区间[3,6],它用红色的[3,5]及[5,6]的并采表示。图中红色结点的count域值为1,黑色结点的count域值为0。
直观地看,子结点就是父结点区间平均分成两部分。设L, R是父结点的区间端点,我们可以增加Lines_Tree.Build(l,r : integer)递归地定义线段树如下:
Procedure Lines_tree.Build(l, r : integer)
1 I ß l {左端点}
2 J ß r {右端点}
3 Count ß 0 {初始化}
4 If r - l > 1 {是否需要生成子结点,若r-l=1则是初等区间}
5 then k ß (l + r) / 2 {平均分为两部分}
6 new(leftchild)
7 leftchild↑.Build(l, k) {建立左子树}
8 new(rightchild)
9 rightchild↑.Build(k, r) {建立右子树}
10 else leftchild ß nil
11 rightchild ß nil
设根结点是Root,建树需要执行Root.Build。
由递归定义看出,线段树是一棵平衡树,高度为┌logN┐。建立整棵树需要的时间为O(N)。
以上着重说明了线段树的存储原理,我们还应建立线段树的基本运算。
线段树可以存储多个区间,所以支持区间插入运算Lines_Tree.Insert(l, r :integer),定义如下:
Procedure Lines_Tree.Insert(l,r : integer)
{[l, r]是待插入区间,l、r都是原始顶点坐标}
1 if (l <= a[i]) and (a[j] <= r)
2 then count ß count + 1 {盖满整个结点区间}
3 else if l< a[(i + j) div 2] {是否能覆盖到左孩子结点区间}
4 then leftchild↑.Insert(l, r) {向左孩子插入}
5 if r > a[(i + j) div 2 ] {是否能覆盖到右孩子结点区间}
6 then rightchild↑.Insert(l, r) {向右孩子插入}
类似地,线段树支持区间的删除Lines_Tree.Delete(l, r : integer),定义如下:
Procedure Lines_Tree.Delete(l,r : integer)
{[l, r]是待删除区间,l、r都是原始顶点坐标}
1 if (l <= a[i]) and (a[j] <= r)
2 then count ß count - 1 {盖满整个结点区间}
3 else if l< a[(i + j) div 2 ] {是否能覆盖到左孩子结点区间}
4 then leftchild↑.Delete(l, r) {向左孩子删除}
5 if r > a[(i + j) div 2 ]{是否能覆盖到右孩子结点区间}
6 then rightchild↑.Delete(l, r) {向右孩子删除}
执行Lines_Tree.Delete(l, r : integer) 的先决条件是区间[l, r]曾被插入且还未删除。如果建树后插入区间[2,5]而删除区间[3,4]是非法的。
通过分析插入与删除的路径,可知Lines_Tree.Insert与Lines_Tree.Delete的时间复杂度均为O(logN)。(详见[附录1])
由于线段树给每一个区间都分配了结点,利用线段树可以求区间并后的测度与区间并后的连续段数。
(一)、 测度
由于线段树结构递归定义,其测度也可以递归定义。增加数据域Lines_Tree.M表示以该结点为根的子树的测度。M取值如下:
a[j] – a[i] 该结点Count>0
M = 0 该结点为叶结点且Count=0
Leftchild↑.M + Rightchild↑.M 该结点为内部结点且Count=0
据此,可以用Lines_Tree.UpData来动态地维护Lines_Tree.M。UpData在每一次执行Insert或Delete之后执行。定义如下:
Procedure Lines_Tree.UpData
1 if count > 0
2 then M ß a[j] – [i] {盖满区间,测度为a[j] – a[i]}
3 else if j - i = 1 {是否叶结点}
4 then M ß 0 {该结点是叶结点}
5 else M ß Leftchild↑.M + Rightchild↑.M
{内部结点}
UpData的复杂度为O(1),则用UpData来动态维护测度后执行根结点的Insert与Delete的复杂度仍为O(logN)。
(二)、 连续段数
这里的连续段数指的是区间的并可以分解为多少个独立的区间。如[1,2]∪[2,3]∪[5,6]可以分解为两个区间[1,3]与[5,6],则连续段数为2。增加一个数据域Lines_Tree.line表示该结点的连续段数。Line的讨论比较复杂,内部结点不能简单地将左右孩子的Line相加。所以再增加Lines_Tree.lbd与Lines_Tree.rbd域。定义如下:
1 左端点I被描述区间盖到
lbd =
0 左端点I不被描述区间盖到
1 右端点J被描述区间盖到
rbd =
0 右端点J不被描述区间盖到
lbd与rbd的实现:
1 该结点count > 0
lbd = 0 该结点是叶结点且count = 0
leftchild↑.lbd 该结点是内部结点且Count=0
1 该结点count > 0
rbd = 0 该结点是叶结点且count = 0
rightchild↑.rbd 该结点是内部结点且Count=0
有了lbd与rbd,Line域就可以定义了:
1 该结点count > 0
Line = 0 该结点是叶结点且count = 0
Leftchild↑.Line + Rightchild↑.Line - 1
当该结点是内部结点且Count=0,Leftchild↑.rbd =1且Rightchild↑.lbd =1
Leftchild↑.Line + Rightchild↑.Line
当该结点是内部结点且Count=0,Leftchild↑.rbd与Rightchild↑.lbd不都为1
据此,可以定义UpData’动态地维护Line域。与UpData相似,UpData’也在每一次执行Insert或Delete后执行。定义如下:
Procedure Lines_Tree.UpData’
1 if count > 0 {是否盖满结点表示的区间}
2 then lbd ß 1
3 rbd ß 1
4 Line ß 1
5 else if j - i = 1 {是否为叶结点}
6 then lbd ß 0 {进行到这一步,如果为叶结点,
count = 0}
7 rbd ß 0
8 line ß 0
9 else line ß Leftchild↑.line + Rightchild↑.line -
Leftchild↑.rbd * Rightchild↑.lbd
{用乘法确定Leftchild↑.rbd与Rightchild↑.lbd是否同时为1}
同时,由于增加了Line、M等几个数据域,在建树Lines_Tree.Build时要将新增的域初始化。
至此,线段树构造完毕,完整的线段树定义如下:
Lines_Tree = object
i, j : integer;
count : integer;
line : integer;
lbd, rbd : byte;
m : integer;
leftchild,
rightchild : ↑Lines_tree;
procedure Build(l, r : integer);
procedure Insert(l, r : integer);
procedure Delete(l, r : integer);
procedure UpData;
procedure UpData’;
end
有了线段树这个工具,可以考虑利用树形结构来描述一组超元线段的状态。
