数据结构课程设计 神秘国度的爱情故事

数据结构

 

课程设计报告

 

 

 

广州大学 计算机科学与网络工程学院

计算机系 17级计科专业2班

 

 

 

2019年6月30日

 

广州大学学生实验报告

开课学院及实验室:计算机科学与工程实验室                      2019年07月01日

学院

计算机科学与网络工程学院

年级/专业/

计科172

姓名

 

学号

 

实验课程名称

数据结构课程设计

成绩

 

实验项目名称

神秘国度的爱情故事

指导老师

 

 

  • 实验目的
  1. 自行设计数据的存储结构
  2. 自行设计数据的数据结构
  3. 能熟练应用所学知识,有一定查阅文献及运用文献资料能力
  4. 能体现创造性思维,或有独特见解

二、实验原理

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/418860.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

前端学习(1805):前端调试之列表伪类练习

index.html <!DOCTYPE html> <html lang"en"><head><!--系统内置 start--><script type"text/javascript"></script><!--系统内置 end--><meta charset"UTF-8"><title>练习</title&g…

Android中的一些基础知识(二)

这几天在回顾Android的基础知识&#xff0c;就把一些常见的知识点整理一下&#xff0c;以后忘了也可以翻出来看一看。 简单介绍一下Activity的生命周期 在API文档中对生命周期回调的函数描述的很详细&#xff0c;这里我只是翻译了一下。 onCreate&#xff1a;当Activity第一次…

关于Open browser failed!! Please check if you have installed the browser chrome correctly!错误的一种解决方法

新建一个文件夹&#xff0c;再在新建的文件夹里写html文件&#xff0c;再用快捷键&#xff1a;altb打开即可。

前端学习(1806):前端调试之列表伪类练习二

index.html <!DOCTYPE html> <html lang"en"><head><!--系统内置 start--><script type"text/javascript"></script><!--系统内置 end--><meta charset"UTF-8"><title>练习</title&g…

虚拟机安装

带你解密Linux的【Vm】-CSDN博客https://blog.csdn.net/lz17267861157/article/details/134031133

推荐开发工具系列之--LinrF5(自动刷新)

最近有点事&#xff1b;略忙&#xff1b;以至于上篇文章说好的明天一直到了今天才到&#xff1b; //*******************************分割是会呼吸的痛****************************** 作为一个程序员&#xff1b;尤其是作为一个网站开发程序员&#xff1b;如果再更尤其点作为一…

前端学习(1807):前端调试之列表伪类练习三

index.html <!DOCTYPE html> <html lang"en"><head><!--系统内置 start--><script type"text/javascript"></script><!--系统内置 end--><meta charset"UTF-8"><title>练习</title&g…

关于win32与win64的兼容性问题

源代码&#xff1a; &#xff08;操作系统作业&#xff09;printf("\nMemory attached at %X\n",(int)shm); shm是个char*地址&#xff1b; gcc编译出现警告&#xff1a;lcylcy-Lenovo-R720-15IKBN:~ $ gcc -o shmread shmread.c shmread.c: In function ‘main’:…

前端学习(1812):前端调试之shadow练习

index.html <!DOCTYPE html> <html lang"en"><head><!--系统内置 start--><script type"text/javascript"></script><!--系统内置 end--><meta charset"UTF-8"><title>练习</title&g…

bullet HashMap 内存紧密的哈希表

last modified time&#xff1a;2014-11-9 14:07:00 bullet 是一款开源物理引擎&#xff0c;它提供了碰撞检測、重力模拟等功能&#xff0c;非常多3D游戏、3D设计软件&#xff08;如3D Mark&#xff09;使用它作为物理引擎。作为物理引擎&#xff0c;对性能的要求是非常苛刻的&…

前端学习(1813):前端调试之微博个人banner开发

index.html <!DOCTYPE html> <html lang"en"><head> <!--系统内置 start--> <script type"text/javascript"></script> <!--系统内置 end--><meta charset"UTF-8"><title>微博实战--ban…

操作系统课设——设计模拟一个SPOOLING假脱机输出程序

广州大学操作系统课程设计报告 要求&#xff1a;书写课程设计报告&#xff0c;报告中应该包含如下内容&#xff1a; 一&#xff0e;课程设计题目及内容 课程设计题目&#xff1a;题目三&#xff1a; 设计模拟一个SPOOLING假脱机输出程序 &#xff08;1&#xff09; 系统设计要…

操作系统实验——进程管理与进程通信

广州大学学生实验报告 实验一 进程管理与进程通信 一、实验目的 1、掌握进程的概念&#xff0c;明确进程的含义。 2、认识并了解进程并发执行的实质&#xff0c;进程的阻塞与唤醒&#xff0c;终止与退出的过程。 3、熟悉进程的睡眠、同步、撤消等进程控制方法。 4、分析进程…

前端学习(1815):前端调试之css flex 练习1

index.html <!DOCTYPE html> <html lang"en"><head><!--系统内置 start--><script type"text/javascript"></script><!--系统内置 end--><meta charset"UTF-8" /><meta name"viewport…

关于操作系统的学习总结

学了一学期的操作系统&#xff0c;就瞎写点东西反思一下吧。 我是广州大学计算机科学与技术专业的&#xff0c;操作系统的知识是比较抽象的。第一看书时必要的。我们的操作系统课程是张艳玲副教授授课的。张老师讲课认真&#xff0c;是一个不错的老师。我们上课用的课本是这本…

iis6 配置python CGI

打开iis管理器&#xff0c;在Web服务扩展中添加一个新的Web服务扩展 点击添加&#xff0c;浏览找到python的目录&#xff0c;文件类型改为CGI exe文件 选择python.exe 然后在目录后添加 %s %s&#xff08;搜了一圈&#xff0c;还没找到原因&#xff09; 确定就行&#xff0c;然…

广州大学专业选修课介绍-----------Linux操作系统分析及实践

教材&#xff1a; 课程内容&#xff1a;你去看一下课本就知道教什么了(●◡●) 考核方式&#xff1a;考察

今天课堂总结

1.带缓存的字符输入输出流 1 package com.xia;2 3 import java.io.BufferedReader;4 import java.io.BufferedWriter;5 import java.io.File;6 import java.io.FileReader;7 import java.io.FileWriter;8 9 public class test { 10 11 public static void main(String[] a…