数据结构课程设计
学院: 计算机科学与教育软件学院
专业班级:网络工程 161 班
姓名:
学号:
000000000000
卟咚君
2018.07.1
1 / 30
广州大学学生实验报告
开课学院及实验室:计算机科学与工程实验室
2018 年 07 月 11 日
学院
实验课
程名称
实验项
目名称
计算机科学与
教育软件学院
年级/专
业/班
网络
161
姓名
卟咚君
数据结构课程设计
神秘国度的爱情故事
学号
成绩
指导老师
张远平
张艳玲
一、 实验目的
1. 自行设计数据的存储结构
2. 自行设计数据的数据结构
3. 能熟练应用所学知识,有一定查阅文献及运用文献资料能力
4. 能体现创造性思维,或有独特见解
5. 能将自己的算法进行优化,并且分析优化的思路
二、实验原理
1、树的广度优先和深度优先的遍历策略
2、时间截,队列的入栈和出栈时间
3、树的结点深度,以及父亲结点的处理
4、求出两个结点的 lca
5、分情况讨论 c 点是否在 a 和 b 结点的路径上
三、实验内容
神秘国度的爱情故事(难度系数 1.5)
题目要求:某个太空神秘国度中有很多美丽的小村,从太空中可以想见,小村
间有路相连,更精确一点说,任意两村之间有且仅有一条路径。小村 A 中有位
年轻人爱上了自己村里的美丽姑娘。每天早晨,姑娘都会去小村 B 里的面包房
工作,傍晚 6 点回到家。年轻人终于决定要向姑娘表白,他打算在小村 C 等
着姑娘路过的时候把爱慕说出来。问题是,他不能确定小村 C 是否在小村 B
到小村 A 之间的路径上。你可以帮他解决这个问题吗?
2 / 30
输入要求:输入由若干组测试数据组成。每组数据的第 1 行包含一正整数 N
( l 《 N 《 50000 ) , 代表神秘国度中小村的个数,每个小村即从 0 到 N -
l 编号。接下来有 N -1 行输入,每行包含一条双向道路的两个端点小村的编
号,中间用空格分开。之后一行包含一正整数 M ( l 《 M 《 500000 ) ,代
表着该组测试问题的个数。接下来 M 行,每行给出 A 、 B 、 C 三个小村的
编号,中间用空格分开。当 N 为 O 时,表示全部测试结束,不要对该数据做
任何处理。
输出要求:对每一组测试给定的 A 、 B 、C,在一行里输出答案,即:如果 C
在 A 和 B 之间的路径上,输出 Yes ,否则输出 No。
思路:在这个课设的题目中,非常巧妙的提出了“任意两村之间有且仅有一条
路径”,我们可以想象的到,这是 n 个结点且刚好有 n-1 条边的连通图,以任
意一个结点为根结点进行广度优先遍历,便会生成一棵树。所以我们处理的方
法就是对一棵树的任意两个结点求他们的最近公共祖先(lca)。这里我定义了
两个结构体 struct Edge 和 struct
Node,Edge 为两个结点(村子)之间的相连的
边,Node 为结点(村子)。Node 有两个变量 adjnode 和 next,adjnode 为结点向
量中的相邻结点,next 为指向下一邻接边的指针。Node 有村子的编号信息,bfs
遍历时的父亲结点,深度信息。用邻接表的方式存储每一个结点与之相连第一
条边,然后每插入一个与之相连的边的时候,都会用头插法的方式更新邻接
表。通过 BFS 预处理出这棵树的结点深度和父亲结点的信息。在查询结点 a 和
结点 b 之间的最近公共祖先的时候,我们可以先把结点深度比较大的结点开始
往上一个一个单位的跳,然后跳到和另外一个结点同样的深度的时候停下来,
查看这时候它们是否在同一一个结点上了,如果是,那这个结点就是它们的最
近公共祖先了,不是的话,我们这次就两个结点一起往它们的父亲结点那里一
个一个单位的跳,直到第一次跳到相同的结点的地方,这时,该结点便是它们
的最近公共祖先。通过课设的题目我们可以知道,任意两个结点之间必定存在
3 / 30
且只有一条路径互达,所以这样处理的方法必定可以找到这两个结点的最近公
共祖先。那么如何解决 C 点是否在 A 和 B 的路径上呢?我们可以先找出
A 和 B 的最近公共祖先为 D,A 和 C 的最近公共祖先为 AC,B 和 C 的最近公共祖
先为 BC。如果 AC==C 并且 BC==C,则说明 C 同时是 A 和 B 的最近公共祖先,这
里需要分情况讨论,如果 C==D 的话,则说明 C 就是 A 和 B 的最近公共祖先,如
果 C!=D,则说明 C 不是 A 和 B 的最近公共祖先,则 A 到 D 再走到 B 的路径中,
不会经过 C 结点。如果只有 AC==C 或者 BC==C,则说明 C 是 A 或者 B 中一个且
只有一个结点的祖先结点。如果 C 是 A 的祖先结点,不是 B 的祖先结点,则说
明 C 在 A 和 D 的路径上,则 C 肯定是在 A 和 B 的路径上。如果 C 是 B 的祖先结
点,不是 A 的祖先结点,则说明 C 在 B 和 D 的路径上,则 C 肯定是在 A 和 B 的
路径上。如果 C 不是 A 和 B 中任意一个结点的祖先结点,那么从 A 到 B 的路径
上不会经过 C 结点。
以下是未优化过的程序的:
时间复杂度为 O(QN),其中,Q 为询问次数,N 为结点(村子)的数量。
(byd001.cpp)
#include
#include
#include
using namespace std;
const int maxn = 50010;
const int DEG = 20;
int edgenum, nodenum;
struct Edge {
//边的邻接点
int adjnode;
Edge *next;
//邻接点在结点向量中的下表
//指向下一邻接边的指针
};
struct Node {
//结点结点(村子)
int id;
int fa;
int depth;
Edge *firstarc;
//结点信息(村子编号)
//结点的父亲结点
//结点的深度
//指向第一个邻接边的指针
4 / 30
};
void creatgraph(Node* node, int n) { //创建一个村落图
Edge *p;
int u, v;
nodenum = n;
edgenum = n - 1;
//结点(村庄)的个数
//边数是结点数-1
for (int i = 0; i> u >> v;
p = new Edge;
p->adjnode = v;
//下面完成邻接表的建立
p->next = node[u].firstarc;
node[u].firstarc = p; //类似于头插法
p = new Edge;
p->adjnode = u;
p->next = node[v].firstarc;
node[v].firstarc = p; //路径是双向的
}
}
void BFS(Node* &node, int root) {
//bfs广度优先遍历,预处理出每一个结点的深度和
父亲结点
queueque;
node[root].depth = 0;
node[root].fa = root;
que.push(root);
while (!que.empty()) {
//队列
//树的根结点的深度为0
//树的根结点的父亲结点为他自己
//根结点入队
int u = que.front();
//将队列的头结点u出队
que.pop();
Edge* p;
for (p = node[u].firstarc; p != NULL; p = p->next) {
//找出和u相连的
边
int v = p->adjnode;
//v为对应邻
5 / 30
接边的邻接结点
是双向边,所以防止再访问到已经访问过的父亲结点
if (v == node[u].fa) continue;
node[v].depth = node[u].depth + 1;
为父亲结点的深度+1
的父亲结点为u
}
}
}
node[v].fa = u;
que.push(v);
//因为存储的
//结点的深度
//记录v结点
//v结点入队
int LCA(Node* node, int u, int v) {
u和结点v的最近公共祖先
//在树node中,找出结点
if (node[u].depth > node[v].depth)swap(u, v);
//u为深度较小的结
点,v为深度较大的结点
int hu = node[u].depth, hv = node[v].depth;
int tu = u, tv = v;
for (int det = hv - hu, i = 1; i <= det; i++)
//两个结点的深度
差为det,结点v先往上跑det个长度,使得这两个结点在同一深度
tv = node[tv].fa;
if (tu == tv)return tu;
度的时候,在同一结点了,那么这个结点就是这两个结点的最近公共祖先
while (tu != tv) {
点,却在同一深度了,那就两个结点一起往上跳一个单位
tu = node[tu].fa;
结点,那这个结点就是它们的最近公共祖先
tv = node[tv].fa;
}
return tu;
}
//如果他们在同一深
//他们不在同一结
//直到跳到同一个
//返回最近公共祖先
void solve(Node* node, int a, int b, int c) {
//在树node中,查询
结点c是否在a和b的路径上
int d = LCA(node, a, b);
的最近公共祖先为d
int ac = LCA(node, a, c);
的最近公共祖先为ac
int bc = LCA(node, b, c);
的最近公共祖先为bc
6 / 30
//找出a和b结点
//找出a和c结点
//找出b和c结点
//cout<> T;
while (T--) {
cin >> n;
Node *node = new Node[n + 1];
creatgraph(node, n);
BFS(node, 0);
7 / 30
//输入测试样例数T
//输入结点数n
//动态申请n+1个结点空间
//建树
//bfs预处理出树结点的深度和对应的父
亲结点
径上
间
}
cin >> m;
while (m--) {
//输入m个询问
cin >> a >> b >> c;
solve(node, a, b, c);
//输入结点编号a,b和c
//询问c结点是否在a和b结点的路
}
delete node;
}
return 0;
//这组测试样例处理结束,撤销申请的空
未优化过的程序,在查找最近公共祖先的时候是一个一个往上面查找的,所以
在最坏的情况下,时间复杂度会退化到 O(QN),比如说在树退化成单链表的时候
就会时间复杂度就会很高。优化的策略是按照倍增思想进行向上查找,比如
说,一个结点需要跳到它的第 7 个祖先结点那里,一次一次的往上跳跃需要 7
次(如图 001 所示),如果把 7 转换成二进制 111,那就是需要往上跳跃三
次,第一次跳跃 4 个单位,第二次跳跃 2 个单位,第三次跳跃 1 个单位就可以
跳跃到第 7 个祖先结点那里(如图 002 所示)。
8 / 30