一、考虑2类指令
Link(v,r): v是一棵树中的结点,r是另一棵树的根,Link的执行使得r成为v的子结点,从而实现两树的合并。
Find-Depth(w):求出结点w的当前深度。
二、D-森林的运用原因:
如果采用前述的树结构形式,且Find-Depth指令不具路径压缩功能, 则执行O(n)条Find-Depth指令,最坏情况下时间复杂度为O(n2);但如果采用具有路径压缩功能的Find-Depth指令,则原先树中在被压缩路径上的各结点深度会发生改变,如不采取其它措施,对其中某结点执行Find-Depth指令时,就会得到错误的深度信息。如果我们给每个结点增加一个字段记录其在原树中的深度,这样,虽可在O(1)时间完成一条Find-Depth指令,但在执行Link指令时,由于以r为根的子树中结点的深度全部发生了变化,我们势必要修改该子树中所有结点的深度字段。此工作量很大,最坏情况下,O(n)条Link指令的时间复杂度也为O(n2);为了既能求得各点在原先树中的正确深度、又能使时间复杂度较小, 我们需要使用具有路径压缩功能的Find-Depth指令;同时我们还需要采取一些辅助手段来保证深度计算的正确性。为此,我们对每个结点v增加2个字段(Count[v]和Weight[v]),并把经过改造的此类结点和树所构成的森林称为D-森林。
三 、D-森林的算法分析
为减小时间复杂度,所有操作均是在D-森林中执行的,但我们必须保证它能正确反映原森林中任何结点在任何时刻的深度。
在上例中,原先对左图中两子树执行Link(r,u)就成为了中图的形状;为使时间复杂度小,Link(r,u)在D-森林中执行后成为右图的形状。虽然形状变了,但我们只要保证在D-森林中执行Find-Depth时,一定能得到
Find-Depth(r)=0, Find-Depth(u)=1, Find-Depth(w)=2;
即保证Find-Depth能给出所有结点在原先森林中的正确深度即可。要实现这一点,首先需要对结点进行改造,即要增加2个字段:
Count[v]: 记录在D-森林中以v为根的子树中结点的个数。
Weight[v]满足下述性质:设在D-森林中执行指令Find-Depth(v),
从v找到根结点a路径上的结点是vi1(=v)vi2vi3…vik(=a), 则等于v在原树中的深度(注意不是在D-森林中的)。(如在上例中,应有Weight[u]=1, Weight[r]=-1, Weight[w]=1,这样就有, Depth(r)= Weight[r]+Weight[u]=0;
Depth(u)= Weight[u]=1;Depth(w)= Weight[w]+Weight[u]=2。)
请注意,初始时即任何Link指令尚未执行(合并)时,不论是在原森林还是在D-森林中,所有结点均为单结点树,故所有结点的Weight值均为0即其深度为0。
(1)Find(i,w,p,Weight)算法
为了保证Weight值所具的性质, 当在D-森林中执行Find指令实施路径压缩时,(即把vi1、vi2、vi3、…vi,k-2变为根结点a(即vik)的子结点时,另外,请注意vi,k-1原本就是根结点a的子结点)要使每一个结点vip (1≤p≤k-2)的新权变为,也就是:
vip的新Weight值= vip的旧Weight 值+vip原父结点的新Weight值
Find(p, i, w, Weight):
{
if ip[i] then {
p[i]Find(p, p[i], father_new_weight, Weight);
Weight[i]Weight[i]+father_new_weight;
wWeight[i];
}
else w0;
return p[i]
}
(2)Find-Depth(v)算法
Find-Depth要给出结点vi在原先未经压缩的森林中的深度Depth(i)。有了经过修改的Find指令,构造Find-Depth(i)指令就很容易了:要计算Depth(i),首先看在D-森林中vi是否为根,即是否有i=p[i],若vi为根,则Weight[i]就是Depth(i);若vi不为根则调用Find(i,w),不论原先vi是否为根a的儿子,执行Find(i,w)后vi一定是根的儿子, 而此时的Depth(i)为Weight[a] +新Weight[i]。
(3)Link(v,r)算法
Link指令仍沿用同一名称,但在D-森林中的执行与在原森林不同,目的仍然是为了减少执行的时间复杂度。D-森林中Link指令的执行要保证原森林中的各点深度能正确计算。
仍以前述图为例
设左上图是Link执行前原森林、D-森林的形态(假定恰好相同)。如Link(r,u)在原森林中执行,则应为上图中间的形态;但Link(r,u)在D-森林中执行后,r,u,w三结点组织结构如右上图所示。
之所以这样处理,其目的是为了减小D-森林中树的高度,这样在执行Find-Depth指令时,所处理的路径长度也就相应短。
由于在原森林中r的深度为0,u的深度为1,w的深度为2,故Link(r,u)在D-森林中执行后,三结点的Weight值应为:Weight[u]=1, Weight[r]=-1, Weight[w]=1。注意这里Weight[u]¹0,即在D-森林中,根的Weight值不一定就是0。另外还需注意的是,
D-森林中的根在原森林中不一定就是根(如u),
原森林中的根在D-森林中也不一定就是根(如r)。
Link(v,r) 的执行:
按原始定义, 在原森林中, v是一棵树中的结点, r是另一棵树的根,Link的执行使得r成为v的子结点,从而实现两树的合并。在D-森林中,把含有v和r的2棵树分别记为Tv和Tr。要注意的是,在D-森林中r不一定是Tr的根,如右上图;但r也有可能是Tr的根,只是执行时无法预测倒底是哪种情况。
在D-森林中执行Link时,并不直接把r做成v的子结点,(在实际执行时,按下述规则有可能碰巧出现这样的情况)而是把结点少的树接到结点多的树的根下,实现两树的合并,同时修改所有应发生变化的结点的Weight值,从而使该树能够正确反映在原森林中把r接在v之下时各结点的深度。
设在D-森林中,Tv和Tr的根分别是v’和r’,合并时按Count[v’]和Count[r’]值的大小分为两种情况:
Count[r’] ≤ Count[v’]和Count[v’]< Count[r’]。
若在D-森林中Count[r’] ≤ Count[v’]则v’作为根, 反之则r’作为根。
1、Count[r’] ≤ Count[v’]
因为要把结点少的树并入结点多的树,故把r’作为v’的儿子。
注意到:Link指令在原森林中执行的目标,就是把r接到v之下;而在原森林中,此种接入对含有v的树中各结点深度不产生任何影响;在D-森林中,现在把Tr接在v’之下,与合并前相比,Tv中的任一条路径也没有受到此接入的任何影响,故在D-森林中,当Count[r’] ≤ Count[v’]即Tv中的结点多于中Tr的结点时, Link执行时Tv树中各结点的Weight值无需修改。但合并时,为使Tr树中各结点的Weight值继续保持其应有的性质,要对Weight[r’]进行修改。
分析:设用Find-Depth指令可以查到v的深度为Depth(v),(注意Depth(v)是执行Find-Depth(v)指令的返回值而不是函数) 而在原先的森林中,由于r是根,故原先有Depth(r)=0(也是值)。执行Link指令后,把r作为v的儿子,
则此时在原先的森林中,r的深度为Depth(v)+1。
设在D-森林中,r’的儿子是s。由Weight的性质,在D-森林中的2棵树合并之前,有:从r到s路径上各结点的权和+旧Weight[r’]=0(r在原森林中的深度)
按上述分析,在原森林中的2棵树合并之后, r的深度为Depth(v)+1; 另一方面,D-森林中对应的2棵树合并后,按Weight的性质,r在原森林中的深度为:从r到s的权和+新Weight[r’]+Weight[v’],因此有从r到s的权和+新Weight[r’]+Weight[v’]= Depth(v)+1。
联立上述两兰色式子可得:
新Weight[r’]=Depth[v]+1-Weight[v’]+旧Weight[r’]。
Tr中的其它各结点的Weight值均无需改变。这是由于Tr中的其它各结点相对于r’的位置在合并后均未改变,故只要r’的新Weight值正确,则Tr中的其它各结点就都能够正确计算出其在原森林中的深度。(更确切地, 设j是Tr中的任一结点, j’是j至r’路径上r’的子结点,合并前,j在原森林中的深度即j相对于r的深度(因r是根),故有j相对于r的深度=j至j’路径上的权值之和+旧Weight[r’];而合并后,j在原森林中的深度为:j相对于r的深度+Depth(v)+1,此值应等于Weight[v’]+新Weight[r’]+ j至j’路径上的权值之和,联立两式可知,利用新Weight[r’],就能正确计算Tr中各结点的深度。)
2、Count[v’]< Count[r’]时,把v’作为r’的儿子。合并后应有:从r到s的权和+新Weight[r’]= Depth(v)+1。故有新Weight[r’]= Depth(v)+1+旧Weight[r’],Tr中的其它各结点的Weight值均无需改变,理由与上小节类似。
另外,Weight[v’]也要修改。设在D-森林中,v’的儿子是t,
则合并前有:从v到t的权和+旧Weight[v’]=Depth(v) (v在原森林里的深度)。
而合并以后应有:从v到t的权和+新Weight[v’]+新Weight[r’]=Depth(v)
(合并时是将r接到v之下,故v在原森林里的深度并不改变。)
2式联立得:新Weight[v’]=旧Weight[v’]-新Weight[r’]。
不论哪一种情况,Weight值修改完后,都要将新根的Count值改为Count[v’]+Count[r’]。另外,在Link程序中需知道v’和r’, 且要用到Depth(v), 以便修改Weight值和Count值。故在Link(v,r)程序中,要执行Find-Depth(v)(以计算出Depth(v)并找到Tv的根v’)、以及改造后的Find(r,w)(以找到Tr的根r’并压缩时修改权值)。
四、作业
设初始时两森林均由单结点树构成。上机依次执行:Link(2,1),
Link(3,2), Link(5,4), Link(4,3), Link(7,6),Link(9,8), Link(8,7),
Find-Depth(6), Link(6,5), Find-Depth(4), Find-Depth(7)。
程序代码:
//head.h文件一 类的定义
#include<iostream>
using namespace std;
#ifndef TreeNode_H
#define TreeNode_H
class TreeNode
{
public:
int count; //以这个点为根的子结点数
int weight; //结点权值
int father; //结点的父结点
int name; //结点名
static int number; // 结点数
TreeNode(); //TreeNode类的构造函数
void Display(); //显示函数
int Find(TreeNode P[],int i,int& lastweight); //寻找根节点
int Loop(TreeNode B[],int k); //查根与结点的关系
int Find_Depth(TreeNode D[],int m,int& root); //计算结点的深度
void Link(TreeNode C[],int nodev,int noder); //结点的连接
};
#endif
//End of file head.h
//main.cpp文件而二 主函数
#include<string.h>
#include<stdlib.h>
#include<stdio.h>
#include<iostream>
#include<windows.h>
using namespace std;
#include"head.h"
int TreeNode::number=0;
int main()
{
TreeNode Dtree; //定义类TreeNode的对象Dtree
TreeNode example[6]; //定义类TreeNode的对象数组example[6]
/*****************给对象数组预设初值****************/
example[1].father=1;
example[1].weight=1;
example[2].father=1;
example[2].weight=-1;
example[3].father=1;
example[3].weight=1;
example[4].father=2;
example[4].weight=3;
example[5].father=4;
example[5].weight=1;
system("color FC");
int root;
int Depth;
for(int i=1;i<6;i++) //显示树的结点数值
{
root=Dtree.Loop(example,i);
example[i].Display();
cout<<" root="<<root<<endl;
}
cout<<endl;
for(int j=1;j<6;j++) //寻找Dtree结点的深度算法
{
Depth=Dtree.Find_Depth(example,j,root);
cout<<"结点"<<j<<" 的深度"<<Depth<<" Dtree根为"<<root<<""<<endl;
}
//Link指令
TreeNode::number=0;
TreeNode D_tree[10]; //定义类TreeNode的对象数组D_tree[10]
cout<<endl<<"建一个九个点的D森林D_tree[10]"<<endl;
Dtree.Link(D_tree,2,1);
Dtree.Link(D_tree,3,2);
Dtree.Link(D_tree,5,4);
Dtree.Link(D_tree,4,3);
Dtree.Link(D_tree,7,6);
Dtree.Link(D_tree,9,8);
Dtree.Link(D_tree,8,7);
cout<<"运行Link(D_tree,2,1);Link(D_tree,3,2);"<<endl
<<"Link(D_tree,5,4);Link(D_tree,4,3);Link(D_tree,7,6);"<<endl
<<"Link(D_tree,9,8);Link(D_tree,8,7);之后"<<endl;
Depth=Dtree.Find_Depth(D_tree,6,root);
cout<<"此时结点6的深度为 "<<Depth<<" 根为"<<root<<endl;
Depth=Dtree.Find_Depth(D_tree,4,root);
cout<<"此时结点4的深度为 "<<Depth<<" 根为"<<root<<endl;
Dtree.Link(D_tree,6,5);
Depth=Dtree.Find_Depth(D_tree,4,root);
cout<<endl<<"再执行完Link(D_tree,6,5)时结点4的深度为 "
<<Depth<<" 根为"<<root<<endl;
Depth=Dtree.Find_Depth(D_tree,7,root);
cout<<"此时结点7的深度为 "<<Depth<<" 根为"<<root<<endl;
cout<<endl<<"D_tree的连接的算法后的结果:"<<endl;
//输出各结点的深度与根节点
for(int k=1;k<10;k++)
{
Depth=Dtree.Find_Depth(D_tree,k,root);
cout<<"结点"<<k<<"的深度是"<<Depth<<" 根为"<<root<<endl;
}
return 0;
}
//End of file main.cpp
//Dtree.cpp文件 类的函数实现
#include"head.h"
TreeNode::TreeNode() //对各结点进行初始化
{
count=0;
weight=0;
father=number;
name=number;
number++;
}
void TreeNode::Display() //显示函数
{
cout<<"结点"<<name-1<<" count="<<count<<" weight="<<weight;
}
//寻找根节点的Find递归调用方法
int TreeNode::Find(TreeNode P[],int i,int& lastweight)
{
if(P[i].father!=i)
{
P[i].father=Find(P,P[i].father,lastweight);
P[i].weight+=lastweight;
lastweight=P[i].weight;
return P[i].father;
}
else
{
lastweight=0;
return i;
}
}
//循环的调用寻找根与各结点的关系
int TreeNode::Loop(TreeNode B[],int k)//
{
int sum=0,root,accumulate=0,start=k;
while(B[k].father!=k)//
{
sum+=B[k].weight;
k=B[k].father;
}
root=k;
k=start;
while(B[k].father!=k)//
{
int temp=B[k].weight;
B[k].weight=sum-accumulate;
accumulate+=temp;
temp=B[k].father;
B[k].father=root;
k=temp;
}
return root;
}
//求取树结点m的深度和根root
int TreeNode::Find_Depth(TreeNode D[],int m,int& root)
{
if(D[m].father==m)
{
root=m;
return D[m].weight;
}
root=Loop(D,m);
return D[m].weight+D[D[m].father].weight;
}
//Link指令:nodev是一棵树中的结点,noder是另一棵树的根,
//Link的执行使得noder成为nodev的子结点,从而实现两树的合并。
void TreeNode::Link(TreeNode C[],int nodev,int noder)
{
int rootv=Loop(C,nodev);
int rootr=Loop(C,noder);
int countv=C[rootv].count;
int countr=C[rootr].count;
int n;
if(countv<=countr)
{
C[rootr].weight=Find_Depth(C,nodev,n)+1-C[rootv].weight+C[rootr].weight;
C[rootv].count+=C[rootr].count+1;
C[rootr].father=rootv;
}
else
{
C[rootr].weight=C[rootr].weight+Find_Depth(C,nodev,n)+1;
C[rootv].weight=C[rootv].weight-C[rootr].weight;
C[rootr].count+=C[rootv].count+1;
C[rootv].father=rootr;
}
}
//End of file Dtree.cpp