数据结构
课程设计报告
广州大学 计算机科学与网络工程学院
计算机系 17级计科专业2班
2019年6月30日
广州大学学生实验报告
开课学院及实验室:计算机科学与工程实验室 2019年07月01日
学院 | 计算机科学与网络工程学院 | 年级/专业/班 | 计科172 | 姓名 |
| 学号 |
|
实验课程名称 | 数据结构课程设计 | 成绩 |
| ||||
实验项目名称 | 神秘国度的爱情故事 | 指导老师 |
|
- 实验目的
- 自行设计数据的存储结构
- 自行设计数据的数据结构
- 能熟练应用所学知识,有一定查阅文献及运用文献资料能力
- 能体现创造性思维,或有独特见解
二、实验原理
1、树的深度优先的遍历策略
2、时间截
3、树的结点深度,以及结点出现顺序的记录
4、求出两个结点的lca
5、分情况讨论c点是否在a和b结点的路径上
6、LCA转RMQ算法
7、邻接表存储结构
8、ST算法的应用
三、实验内容
神秘国度的爱情故事(难度系数 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)。这里我采用了用邻接表的方式存储图,然后每插入一个与之相连的边的时候,都会用头插法的方式更新邻接表。双向边的处理则是通过两次插入来实现。通过DFS预处理出这棵树的结点深度和结点出现次序的信息。在查询结点a和结点b之间的最近公共祖先的时候,我们可以找出结点a和结点b首次出现的次序,然后在此区间内找到深度最小的结点(这里涉及RMQ算法:即区间求最值的问题),该节点就是结点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。
随机生成树的算法我是采用了比较简单的算法,算法如下:
#include<cstdio>
#include<cstdlib>
#include<ctime>
#include<iostream>
using namespace std;
typedef long long ll;
int random(int n)
{
return (ll)rand() * rand() % n;
}
int main()
{
srand((unsigned)time(0)); // 初始化随机种子
//生成n个点,n-1条边,附带1e9的权值的树
for(int i = 2; i <= n; ++i)
{ //从点i向1~i-1 之间的点随机连一条边
int fa = random(i - 1) + 1;
int val = random(1000000000) + 1;
printf("%d %d %d\n", fa, i, val);
}
return 0;
}
主程序代码:
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <iostream>
#include <stdio.h>
#include <malloc.h>
#include<ctime>
#define MAXN 1010
#define MAXM 100000
#define MAXV 1000
typedef long long ll;
using namespace std;
int random(int n)//生成小于n的随机数
{
return (ll)rand() * rand() % n;
}
typedef char InfoType;
//以下定义邻接矩阵类型
typedef struct
{
int no; //顶点编号
InfoType info; //顶点其他信息
} VertexType; //顶点类型
//以下定义邻接表类型
typedef struct ANode
{
int adjvex; //该边的邻接点编号
struct ANode* nextarc; //指向下一条边的指针
int weight; //该边的相关信息,如权值(用整型表示)
} ArcNode; //边结点类型
typedef struct Vnode
{
InfoType info; //顶点其他信息
int count; //存放顶点入度,仅仅用于拓扑排序
ArcNode* firstarc; //指向第一条边
} VNode; //邻接表头结点类型
typedef struct
{
VNode adjlist[MAXV]; //邻接表头结点数组
int n, e; //图中顶点数n和边数e
} AdjGraph; //完整的图邻接表类型
//-----------------------------------------------------------
int vs[MAXN << 1];//第i次DFS访问节点的编号
int depth[MAXN << 1];//第i次DFS访问节点的深度
int id[MAXN];//id[i] 记录在vs数组里面 i节点第一次出现的下标
int dfs_clock;//时间戳
int e,n;//点数 边数 查询数
int dp[MAXN << 1][20];//dp[i][j]存储depth数组 以下标i开始的,长度为2^j的区间里 最小值所对应的下标
//----邻接表的基本运算算法------------------------------------
void Init_CreateAdj(AdjGraph*& G,int n, int e) //初始化图的邻接表
{
int i, j;
ArcNode* p;
G = (AdjGraph*)malloc(sizeof(AdjGraph));
for (i = 1; i <= n; i++) //给邻接表中所有头结点的指针域置初值
G->adjlist[i].firstarc = NULL;
G->n = n; G->e = e;
}
void AddNode(AdjGraph*& G, int u, int v) //由边集创建图
{
ArcNode* p;
p = (ArcNode*)malloc(sizeof(ArcNode)); //创建一个结点p
p->adjvex = v;
p->nextarc = G->adjlist[u].firstarc; //采用头插法插入结点p
G->adjlist[u].firstarc = p;
}
void DispAdj(AdjGraph* G) //输出邻接表G
{
int i;
ArcNode* p;
for (i = 1; i <= G->n; i++)
{
p = G->adjlist[i].firstarc;
printf("%3d: ", i);
while (p != NULL)
{
printf("%3d[%d]→", p->adjvex, p->weight);
p = p->nextarc;
}
printf("∧\n");
}
}
void DestroyAdj(AdjGraph*& G) //销毁图的邻接表
{
int i;
ArcNode* pre, * p;
for (i = 1; i < G->n; i++) //扫描所有的单链表
{
pre = G->adjlist[i].firstarc; //p指向第i个单链表的首结点
if (pre != NULL)
{
p = pre->nextarc;
while (p != NULL) //释放第i个单链表的所有边结点
{
free(pre);
pre = p; p = p->nextarc;
}
free(pre);
}
}
free(G); //释放头结点数组
}
//------------------------------------------------------------
//深度优先遍历算法
int visited[MAXV] = { 0 };
void DFS(AdjGraph* G, int v, int d)
{
ArcNode* p;
visited[v] = 1; //置已访问标记
//printf("%d ", v);//输出被访问顶点的编号
id[v] = dfs_clock;//记录第一次出现的位置
vs[dfs_clock] = v;
depth[dfs_clock++] = d;
p = G->adjlist[v].firstarc; //p指向顶点v的第一条弧的弧头结点
while (p != NULL)
{
if (visited[p->adjvex] == 0) //若p->adjvex顶点未访问,递归访问它
{
DFS(G, p->adjvex, d + 1);
vs[dfs_clock] = v;//类似 回溯
depth[dfs_clock++] = d;
}
p = p->nextarc; //p指向顶点v的下一条弧的弧头结点
}
}
//---------------------------------------------------------
void find_depth(AdjGraph* G)
{
dfs_clock = 1;
memset(vs, 0, sizeof(vs));
memset(id, 0, sizeof(id));
memset(depth, 0, sizeof(depth));
DFS(G, 1, 0);//遍历
}
void RMQ_init(int NN)//预处理 区间最小值 预处理ST表,数组中共NN个元素
{
for (int i = 1; i <= NN; i++)
dp[i][0] = i;//初始化
for (int j = 1; (1 << j) <= NN; j++)
{
for (int i = 1; i + (1 << j) - 1 <= NN; i++)
{
int a = dp[i][j - 1];
int b = dp[i + (1 << (j - 1))][j - 1];
if (depth[a] <= depth[b])//f[i][j] = max(f[i][j - 1], f[i + (1 << (j - 1))][j - 1]);这里把a,b注入dp数组中,所以dp里面存的是不同结点,达到比较深度,从而得出结点的目的
dp[i][j] = a;//比如dp[1][3]是指从第一个位置开始,长度为2^3的序列里深度最小的结点
else
dp[i][j] = b;
}
}
}
int query(int L, int R)//查询操作 ans = max(f[l][k], f[r - (1 << k) + 1][k]);
{
//查询L <= i <= R 里面使得depth[i]最小的值 返回对应下标
int k = 0;
while ((1 << (k + 1)) <= R - L + 1) k++;
int a = dp[L][k];
int b = dp[R - (1 << k) + 1][k];
if (depth[a] <= depth[b])
return a;
else
return b;
}
int LCA(int u, int v)
{
int x = id[u];//比较大小 小的当作左区间 大的当作右区间
int y = id[v];
if (x > y)
return vs[query(y, x)];
else
return vs[query(x, y)];
}
void solve(int a, int b, int c)
{
int d = LCA(a, b); //找出a和b结点的最近公共祖先为d
int ac = LCA(a, c); //找出a和c结点的最近公共祖先为ac
int bc = LCA(b, c); //找出b和c结点的最近公共祖先为bc
bool flag = false;
if (ac == c && bc == c) { //如果ac==c并且bc==c,说明c结点是a和b结点的公共祖先
if (c == d) { //如果c==d,说明c就是a和b的最近公共祖先,c必定在a和b的路径上
flag = true;
}
else
flag = false; //如果c!=d,说明c不是a和b的最近公共祖先,a和b的路径上不包括c
}
else if (ac == c || bc == c) { //c是a的祖先或者是b的祖先,说明c在a到d的路径上或者在b到d的路径上
flag = true; //此时c一定是a和b路径上的点
}
else {
flag = false; //如果c不是a的祖先,也不是b的祖先,则a和b的路径上不会经过c点
}
if (flag)
cout << "村子" << c << "在村子" << a << "和村子" << b << "的路径上" << endl;
else
cout << "村子" << c << "不在村子" << a << "和村子" << b << "的路径上" << endl;
}
void input()
{
printf("下标: ");
for (int i = 1; i < dfs_clock; i++)
printf("%d ", i);
printf("\n");
printf("vs: ");
for (int i = 1; i < dfs_clock; i++)
printf("%d ", vs[i]);
printf("\n");
printf("depth: ");
for (int i = 1; i < dfs_clock; i++)
printf("%d ", depth[i]);
printf("\n");
printf("下标: ");
for (int i = 1; i <= (e+1); i++)
printf("%d ", i);
printf("\n");
printf("id: ");
for (int i = 1; i <= (e + 1); i++)
printf("%d ", id[i]);
printf("\n");
}
int main()
{
AdjGraph* G;
cout << "输入村庄数和边数" << endl;
cin >> n >> e;
Init_CreateAdj(G,n, e); //建立邻接表
cout << "下面开始自动生成n个结点的树:"<<endl;
srand((unsigned)time(0)); // 初始化随机种子
for (int i = 2; i <= n; ++i)
{
int fa = random(i - 1) + 1;
AddNode(G, i, fa);
AddNode(G, fa, i);
}
printf("图G的邻接表:\n");
DispAdj(G); //输出邻接表G
find_depth(G);//DFS遍历整个树 求出所需要的信息
RMQ_init(dfs_clock - 1);
input();
int m, a, b, c;
cout << "输入样例结束,请输入查询次数:" << endl;
cin >> m;
cout << "请输入A,B,C" << endl;
while (m--) {
cin >> a >> b >> c; //输入结点编号a,b和c
solve(a, b, c); //询问c结点是否在a和b结点的路径上
}
DestroyAdj(G);
return 0;
}
四,程序验证
程序运行结果:
六.算法的时间复度
由于本题采用了ST算法,ST算法的时间包括复杂度是O(nlogn)的建表和O(1)的查询。另外本题采用邻接表的存储结构,在进行一次深度优先遍历时,最坏时可能需要将链表中所有结点都遍历完(尤其是有向图中),此时时间复杂度自然就是O(e)了。
五.存在的问题及体会
在本次实验报告中,选择了第三个课题“神秘国度的爱情故事”。
在一开始选择实现的算法的时候,我主要考虑了能够简单解决问题的暴力(即每次查询均进行广度优先遍历)算法,这个算法简答明了易懂,但效率是低效的,然后我上网查询了相关资料决定采用LCA+ST算法,LCA+ST算法可以在预处理一遍N个结点后,很稳定的在O(ln(N))的时间复杂度找出任意两个结点之间的最近公共祖先,然后通过AB,AC,BC之间的最近公共祖先的关系可以判断C是否在A和B结点的路径上的问题。我的做法是基于dfs深度优先搜索的,在深度优先的过程中,把结点的深度信息和结点出现的次序即遍历的次序分别存在三个数组里,所以LCA+ST的做法实际是牺牲了空间来争取时间。如果村庄的数量极其庞大可能程序会出现问题。因此这是需要解决的问题。
数据结构是计算机专业学生核心的课程,学好这门课程需要有耐性,有恒心,不断努力,不断实践。最后衷心感谢老师这一学期来的教导!
参考网站:https://blog.csdn.net/chenzhenyu123456/article/details/47359859
https://blog.csdn.net/forever_dreams/article/details/81127189