神秘国度的爱情故事
数据结构课设-广州大学
ps:本次课设程序不仅需要解决问题,更需要注重代码和算法的优化和数据测试分析
直接广度优先实现的方法时间复杂度为O(QN),优化后的方法是lca+倍增思想,时间复杂度为O(Qln(N))
是自己写的随机生成树和随机生成询问,输入数据保证准确,进行数据测试分析。
因为在博客中不在方便调整排版,所以在这里给出word文档的下载地址(内容和这次博客的内容完全一致)
此外,还有一个压缩包,包括4个cpp源文件,21组测试样例,以及上面那个完整的word文档,下载地址
三、实验内容
神秘国度的爱情故事(难度系数 1.5)
题目要求:某个太空神秘国度中有很多美丽的小村,从太空中可以想见,小村间有路相连,更精确一点说,任意两村之间有且仅有一条路径。小村 A 中有位年轻人爱上了自己村里的美丽姑娘。每天早晨,姑娘都会去小村 B 里的面包房工作,傍晚 6 点回到家。年轻人终于决定要向姑娘表白,他打算在小村 C 等着姑娘路过的时候把爱慕说出来。问题是,他不能确定小村 C 是否在小村 B 到小村 A 之间的路径上。你可以帮他解决这个问题吗?
输入要求:输入由若干组测试数据组成。每组数据的第 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之间的最近公共祖先的时候,我们可以先把结点深度比较大的结点开始往上一个一个单位的跳,然后跳到和另外一个结点同样的深度的时候停下来,查看这时候它们是否在同一一个结点上了,如果是,那这个结点就是它们的最近公共祖先了,不是的话,我们这次就两个结点一起往它们的父亲结点那里一个一个单位的跳,直到第一次跳到相同的结点的地方,这时,该结点便是它们的最近公共祖先。通过课设的题目我们可以知道,任意两个结点之间必定存在且只有一条路径互达,所以这样处理的方法必定可以找到这两个结点的最近公共祖先。那么如何解决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<iostream>
#include<cstring>
#include<queue>
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; //指向第一个邻接边的指针
};
void creatgraph(Node* node, int n) { //创建一个村落图
Edge *p;
int u, v;
nodenum = n; //结点(村庄)的个数
edgenum = n - 1; //边数是结点数-1
for (int i = 0; i<nodenum; i++) {
node[i].id = i; //每个小村从0到N-1编号
node[i].firstarc = NULL; //每一个村庄的第一个邻接边的指针初始化为NULL
}
//接下来有n-1行输入,每行包含一条双向的两个端点小村的编号,中间用空格分开。
//cout << "请输入村子的" << n - 1 << "条路径(两个端点中间用空格分隔):" << endl;
for (int i = 1; i <= edgenum; i++) {
cin >> 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广度优先遍历,预处理出每一个结点的深度和父亲结点
queue<int>que; //队列
node[root].depth = 0; //树的根结点的深度为0
node[root].fa = root; //树的根结点的父亲结点为他自己
que.push(root); //根结点入队
while (!que.empty()) {
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为对应邻接边的邻接结点
if (v == node[u].fa) continue; //因为存储的是双向边,所以防止再访问到已经访问过的父亲结点
node[v].depth = node[u].depth + 1; //结点的深度为父亲结点的深度+1
node[v].fa = u; //记录v结点的父亲结点为u
que.push(v); //v结点入队
}
}
}
int LCA(Node* node, int u, int v) { //在树node中,找出结点u和结点v的最近公共祖先
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); //找出a和b结点的最近公共祖先为d
int ac = LCA(node, a, c); //找出a和c结点的最近公共祖先为ac
int bc = LCA(node, b, c); //找出b和c结点的最近公共祖先为bc
//cout<<d<<" "<<ac<<" "<<bc<<endl;
if (ac == c&&bc == c) { //如果ac==c并且bc==c,说明c结点是a和b结点的公共祖先
if (c == d) { //如果c==d,说明c就是a和b的最近公共祖先,c必定在a和b的路径上
cout << "Yes" << endl;
}
else
cout << "No" << endl; //如果c!=d,说明c不是a和b的最近公共祖先,a和b的路径上不包括c
}
else if (ac == c || bc == c) { //c是a的祖先或者是b的祖先,说明c在a到d的路径上或者在b到d的路径上
cout << "Yes" << endl; //此时c一定是a和b路径上的点
}
else {
cout << "No" << endl; //如果c不是a的祖先,也不是b的祖先,则a和b的路径上不会经过c点
}
}
int main() {
#ifndef OnLINE_JUGE
//freopen("D:\\test_in.txt","r",stdin); //数据输入为测试数据文件test_in.txt的内容
//freopen("D:\\test_out_1.txt","w",stdout); //数据输出到结果文件test_out_1.txt中
#endif
int T, n, m; //T为测试样例个数,每一组测试样例包括n个村子,n-1个村子之间的关系,m组询问