看前须知
第五次上机题汇总
实验:树的构造与遍历——根据提示循序渐进(可惜提示有问题Ծ‸Ծ).
题目内容
问题描述
某单位信息网络结构呈树型结构,网络中节点可为交换机、计算机和打印机三种设备,计算机和打印机只能位于树的叶节点上。如要从一台计算机上打印文档,请为它选择最近(即经过交换机最少)的打印机。
在该网络结构中,根交换机编号为0,其它设备编号可为任意有效正整数,每个交换机有8个端口(编号0-7)。当存在多个满足条件的打印机时,选择按树前序遍历序排在前面的打印机。
输入形式
首先从标准输入中输入两个整数,第一个整数表示当前网络中设备数目,第二个整数表示需要打印文档的计算机编号。两整数间以一个空格分隔。假设设备总数目不会超过300。
然后从当前目录下的in.txt读入相应设备配置表,该表每一行构成一个设备的属性,格式如下:
<设备ID> <类型> <设备父节点ID> <端口号>
<设备ID>为一个非负整数,表示设备编号;<类型>分为:0表示交换机、1表示计算机、2表示打印机;<设备父结点ID>为相应结点父结点编号,为一个有效非负整数;<端口号>为相应设备在父结点交换机中所处的端口编号,分别为0-7。由于设备配置表是按设备加入网络时的次序编排的,因此,表中第一行一定为根交换机(其属性为0 0 -1 -1);其它每个设备结点一定在其父设备结点之后输入。每行中设备属性间由一个空格分隔,最后一个属性后有换行符。
输出形式
向控制台输出所选择的打印机编号,及所经过的交换机的编号,顺序是从需要打印文档的计算机开始,编号间以一个空格分隔。
样例
【样例输入】
37 19
in.txt中的信息如下:
0 0 -1 -1
1 0 0 0
2 0 1 2
3 1 1 5
4 0 0 1
5 1 4 0
6 2 2 2
7 0 4 2
8 0 0 4
9 0 2 0
10 0 9 0
11 2 10 3
12 0 9 2
13 0 7 0
14 0 13 0
15 2 7 3
16 0 8 1
17 0 16 0
18 1 17 5
19 1 9 5
20 0 12 1
21 1 14 1
22 1 14 2
23 1 13 2
24 1 12 5
25 0 20 1
26 1 20 2
27 0 14 7
28 0 16 1
29 1 4 3
30 0 16 7
31 0 28 0
32 2 31 0
33 1 30 2
34 1 31 2
35 0 31 5
36 1 35 3
【样例输出】
11 9 10
样例说明
样例输入中37表示当前网络共有37台设备,19表示编号为19的计算机要打印文档。in.txt设备表中第一行0 0 -1 -1表示根节点交换机设备,其设备编号为0 、设备类型为0(交换机)、父结点设备编号-1表示无父设备、端口-1表示无接入端口;设备表第二行1 0 0 0表示设备编号为1 、设备类型为0(交换机)、父结点设备编号0(根交换机)、端口0表示接入父结点端口0;设备表中行5 1 4 0表示设备编号为5 、设备类型为1(计算机)、父结点设备编号4、端口0表示接入4号交换机端口0;设备表中行6 2 2 2表示设备编号为6 、设备类型为2(打印机)、父结点设备编号2、端口2表示接入2号交换机端口2。
样例输出11 9 10表示选择设备编号为11的打印机打印文档,打印需要经过9号和10号交换机(尽管6号和11号打印机离19号计算机距离相同,但11号打印机按树前序遍历时排在6号之前)。
题解
笔者的发泄(内含思维过程)和详解
笔者的发泄:这道题也太变态了吧(当然也有可能是我太菜了),这道题是在5.1写的,花了将近三个小时,从一开始的毫无头绪,到会处理数据,到采用多种方法,不断淘汰垃圾算法,最后到AC,实属不易。看过这篇文章而且有收获的不放点一个赞吧,不然感觉敲这么多字没有成就感。
一开始我一看到最短距离我就想到了树中的最短路径算法(就是把一颗树转变成一个无向图Digraph,然后用Dijkstra算法)但是这个完全就不是树的考点,而且这是多叉树,不是二叉树,所以我很快就放弃了。但是此时我对怎么处理这么多数据,怎么构建二叉树我是毫无头绪的,毕竟这个数据既包含了父亲节点的编号又有自己的编号,还有父亲节点的端口号,如果采用一般的多叉树(或者是多叉搜索树)来构建,光录入信息就是一个麻烦事,所以我就再寻思怎么构建。后来我想到了一个绝妙的方法,就是用数组来模拟树(树形数组,或树状数组),这种方法实属不多见,因为需要的信息十分多,但是正好这题的信息正好满足,所以采用这种数据结构是在是妙啊。
好啦,知道用数组来模拟树了,那么有没有思考过怎么来实现前序遍历呢,其实很简答——递归(DFS)+队列 就可以实现了,详解看代码,这里不多赘述。现在到最难的操作的地方了,现在你知道了前序遍历的结果,也知道找到了按前序遍历序的打印机,怎么选择距离最短的打印机?(我在这里花费了近两个小时)首先我们必须要注意到,打印机和要求的计算机可不一定在一个子树上呀,是有可能跨越根节点呀(嘘,有一个测试点就是这个)。我一开始是这样想的,我们可以这样干:把计算机所在的那个子树特别标记(加权咯),打印机在这个子树上的计算距离和打印机不在这个子树上的计算距离方式不同但是还是绕不开怎么去计算。期间我思考了一个多小时,突然想起了一个算法——二叉树寻找最早公共祖先,如果我们找到计算机和各个打印机的公共祖先,然后通过公共祖先来计算不就可以得到两点的路径了吗。但是怎么寻找最早公共祖先这个经典算法我也在这里不在赘述,由于这是多叉树,而且这个数组模拟的树不仅有自己的ID,还有父亲的ID,那么让这两个节点同时退回根节点,其中第一个相同的不就是最早公共祖先了吗(真的妙啊哈哈哈哈哈哈哈)。最后就是一个小难点,就是怎么输出经过的交换机编号,这里,我明确强调,一定要多采用父亲节点的ID(因为这是一对一的,而倒过来是一对多的),再巧妙使用栈就可以解决了。
参考代码
#include<stdio.h>
#include<string.h>
#include<math.h>
#include<stdlib.h>
#include<ctype.h>
#include<stdbool.h>
#define MAX 2000
typedef struct TreeNode{ //简化的树形数组
int ID; //树节点的编号
int type; //树节点类型 (0表示交换机、1表示计算机、2表示打印机)
int parentID; //父结点编号
int portNumber; //父结点交换机中所处的端口编号,分别为0-7
struct TreeNode *child[9]; //树节点的子树地址8个端口(编号0-7) ************注意,至少要开 9,如果是 8可能会越界
}Tree,*Treep;
typedef struct queue{ //前序遍历的存储队列
int ID; //树节点的编号
int type; //树节点类型
int parentID; //父结点编号
int portNumber; //父结点交换机中所处的端口编号
int when; //树节点第几次被访问 (前序遍历编号)
int pathNum; //一开始记录的是节点到根之间的距离,之后是计算机到打印机之间的距离
int path[200]; //记录节点到根之间的节点编号
int ancestor; //共同祖先
}queue;
Tree node[MAX]; //树形数组
queue q[MAX*7],qq[MAX*7],computer;//q是前序遍历构造的包含全部系统的队列,qq是前序遍历构造的含打印机和要求的计算机的队列,computer存储是要求计算机的内容
int i,j,n,head,target,printNum,top,ancestor,stack[300];//head是队头,target记录要求计算机的位置,printNum是打印机的总数,top是栈顶,ancestor是公共祖先,stack是栈
int ID,type,parentID,portNumber,computerID;//输入参数
void CreateTreeNode(FILE *fp); //构建树形数组
void SortTree(Treep root); //递归处理树形数组
void CreateQueue(Treep root); //前序遍历构造队列
int returnRoot(int ID,int pathNum); //返回根,同时记录途中信息
int findSameAncestor_and_calculatePath(int *a,int *b,int la,int lb);//找到公共祖先,同时记录两节点之间的距离
int cmp(const void*p1,const void*p2); //比较函数
int main()
{
FILE *fp = fopen("in.txt","r");
scanf("%d %d",&n,&computerID);
CreateTreeNode(fp); //用树形数组模拟多叉树
SortTree(&node[0]); //递归处理树形数组
for(i=0;i<head;i++) //构造qq, qq是前序遍历构造的含打印机和要求的计算机的队列
{
if(q[i].type==2 || q[i].ID==computerID)
{
if(q[i].ID==computerID) target=printNum;//target记录要求计算机的位置
qq[printNum].ID=q[i].ID;
qq[printNum].type=q[i].type;
qq[printNum].parentID=q[i].parentID;
qq[printNum].portNumber=q[i].portNumber;
qq[printNum].when=q[i].when;
printNum++;
}
}
for(i=0;i<printNum;i++)
{
top=0;
qq[i].pathNum=returnRoot(qq[i].ID,0); //计算 节点到根之间的距离,以及 记录节点到根之间的节点编号
}
computer.ID=computerID; //用computer拷贝target记录的信息
computer.type=qq[target].type;
computer.parentID=qq[target].parentID;
computer.portNumber=qq[target].portNumber;
computer.when=qq[target].when;
computer.pathNum=qq[target].pathNum;
memcpy(computer.path,qq[target].path,sizeof(qq[target].path)); //要用memcpy
for(i=0;i<printNum;i++)
{
ancestor=0;
qq[i].pathNum=findSameAncestor_and_calculatePath(qq[i].path,computer.path,qq[i].pathNum-1,computer.pathNum-1); 找到要求计算机和各个打印机的公共祖先,同时记录两者之间的距离
qq[i].ancestor=ancestor;
}
qq[target].pathNum=0x3f3f3f3f; //一定不要忘了这一步操作,由于qq里还有计算机本身,如果不把其距离设为无穷大,不然距离最近的永远是它本身
qsort(qq,printNum,sizeof(queue),cmp);//排序
printf("%d ",qq[0].ID); //打印打印机编号
ID=node[computerID].parentID;
while(ID!=qq[0].ancestor)
{
printf("%d ",ID); //输出计算机到公共祖先的节点ID
ID=node[ID].parentID;
}
printf("%d ",qq[0].ancestor);//输出公共祖先的节点ID
top=0;
ID=qq[0].parentID;
while(ID!=qq[0].ancestor)
{
stack[top]=ID;top++; //先存在栈里,等一下要倒着输出打印机到公共祖先的节点ID
ID=node[ID].parentID;
}
while(top--)
printf("%d ",stack[top]); //倒着输出打印机到公共祖先的节点ID
return 0;
}
int findSameAncestor_and_calculatePath(int *a,int *b,int la,int lb)
{
int times=0,pathnum=0,x,y;
for(x=0;x<lb;x++){
for(y=0;y<la;y++){
if(b[x]==a[y] && times==0){ //由于公共祖先只有一个,所以靠times来判断
ancestor=a[y]; //记录公共祖先
pathnum++; //公共路径长度+1
times++;
break;
}
else if(b[x]==a[y]){
pathnum++; //公共路径长度+1
break;
}
else continue;
}
}
return la+lb-2*pathnum+1;
}
int returnRoot(int ID,int pathNum)
{
if(ID==0) //回到根节点
{
qq[i].pathNum=pathNum; //记录到根之间的距离
return pathNum;
}
else
{
qq[i].path[top]=node[ID].parentID;//记录返回根之间需要经过的节点ID
top++;
returnRoot(node[ID].parentID,pathNum+1);
}
}
void CreateTreeNode(FILE *fp)
{
while(n--)
{
fscanf(fp,"%d %d %d %d",&ID,&type,&parentID,&portNumber); //录入数据
if(parentID==-1) //录入根节点信息
{
node[0].ID=node[0].type=0;
node[0].parentID=node[0].portNumber=-1;
}
else //录入一般节点信息
{
node[ID].ID=ID;
node[ID].type=type;
node[ID].parentID=parentID;
node[ID].portNumber=portNumber;
node[parentID].child[portNumber]=&node[ID];//树形数组相连,巧妙模拟树
}
}
}
void SortTree(Treep root)
{
int j;
CreateQueue(root); //构造前序遍历的存储队列
for(j=0;j<=7;j++)
{
if(root->child[j]!=NULL)
{
SortTree(root->child[j]);//进行递归
}
}
return ;
}
void CreateQueue(Treep root) //构造队列录入信息
{
q[head].ID=root->ID;
q[head].type=root->type;
q[head].portNumber=root->portNumber;
q[head].parentID=root->parentID;
q[head].when=head;//记录前序遍历的时间
head++;
}
int cmp(const void*p1,const void*p2)
{
struct queue *a=(struct queue*)p1;
struct queue *b=(struct queue*)p2;
if(a->pathNum!=b->pathNum) return a->pathNum-b->pathNum; //先比两节点的距离
else return a->when-b->when ; //再比前序遍历的时间
}
补充测试的数据
【样例输入】
38 37
in.txt中的信息如下:
0 0 -1 -1
1 0 0 0
2 0 1 2
3 1 1 5
4 0 0 1
5 1 4 0
6 2 2 2
7 0 4 2
8 0 0 4
9 0 2 0
10 0 9 0
11 2 10 3
12 0 9 2
13 0 7 0
14 0 13 0
15 2 7 3
16 0 8 1
17 0 16 0
18 1 17 5
19 1 9 5
20 0 12 1
21 1 14 1
22 1 14 2
23 1 13 2
24 1 12 5
25 0 20 1
26 1 20 2
27 0 14 7
28 0 16 1
29 1 4 3
30 0 16 7
31 0 28 0
32 2 31 0
33 1 30 2
34 1 31 2
35 0 31 5
36 1 35 3
37 1 8 7
【样例输出】
6 8 0 1 2