结论:最开始N个结点的树,处理M组数据,采用深度优先搜索,总时间复杂度为O(NM)。优化方法是找最近公共祖先(lca)的倍增法。N个结点的树,每次找最近公共祖先的时间复杂度为O(logN),处理M组数据,总时间复杂度为O(MlogN)。

一、实验目的

  1. 理解问题的要求并设计出合理的方法来解决。
  2. 选择适合的数据结构处理问题。
  3. 设计算法解决问题。
  4. 通过比较算法的时间复杂度和空间复杂度来选择更优的算法。

二、使用仪器、器材

微机一台

操作系统:Win10

编程软件:Visual Studi0 2015 C++

三、实验内容及原理

实验内容:

某个太空神秘国度中有很多美丽的小村,从太空中可以想见,小村间有路相连,更精确一点说,任意两村之间有且仅有一条路径。小村 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.

算法设计思路:

1、在拿到题目后,当看到“任意两村之间有且仅有一条路径”,可以知道这些(小村)结点是没有回路的,也就是构成一颗树的形状,所以首先想到的是用树的方法处理这道题。

2、在接下来阅读题目,发现是要找C是否在AB之间,首先想到采用从A遍历到B的过程中判断是否经过C点。但是在树的遍历有先序、中序和后序,都不适用于路径的查找。同时,和一个(小村)结点相连的结点可能不止两个,用学过的二叉树存储肯定不行。所以采用了图的方式来处理这颗树,存储方式是邻接表,遍历方式采用了深度优先搜索。

3、如果采用深度优先搜索,最坏的情况是每个结点都要遍历一次,所以它的时间复杂度是O(N),N是结点的个数。另外每一组测试数据都要处理,时间复杂度是O(M),M是测试数据的个数。每一组测试数据都要深度优先搜索遍历一遍,所以总的时间复杂度是O(NM),当N、M很大时,这是相当耗费时间的,要优化算法。

4、第一个优化是在创好邻接表后,先以0为根结点深度优先遍历一次,把每个结点的深度和父结点找到并存储起来。处理一组数据时,只要知道A和B的深度并都往上遍历到根结点,在这个过程中判断是否经过C点就行。但有一种情况,就是A和B往上走时很快在某个祖先结点相遇,所以在接下来走到根结点的过程中可能会出错、误判,或者因为重复而导致时间耗费。所以继续优化,采用找最近公共祖先。

5、最近公共祖先,顾名思义,就是两个结点的共同的祖先,同时也是距离两个结点最近的。如下图中9、11的最近公共祖先是0,12、10的最近公共祖先是5。思路是先让AB中深度大的结点先往上走到和深度小的结点同深度,然后两个结点同时往上走并判断是否相等,相等即找到最近公共祖先,在这个过程中要判断是否经过C。

但是,当这棵树是一颗单链的树,这些优化并不起很好作用,时间复杂度还是O(NM)。通过查找资料,最后采用了处理最近公共祖先的算法之一-----倍增法。

6、找公共祖先所用的倍增法基本原理其实就是往上走的时候不是采用一步步走(如图1),而是采用往上跳2的倍数的方法。比如(图2)中,12可以往上跳1步到9,可以往上跳2步到5,也可以往上跳4步到根结点0。要使用倍增法,必须先预处理一个用来倍增的数组,里面存储了每个结点往上倍增的信息,预处理的时间复杂度是O(NlogN),N是结点个数。

      

图1                                                    图2

预处理我用grand[50000][30]数组来存储,第一个是结点的编号;第二个是倍增的距离,是2的次方,比如grand[30][3]表示结点30往上跳2^3=8距离的结点编号。因为之前第一次处理时获取了每个结点的深度和父结点,grand[xx][0]是往上跳1步,也就是父结点。下面演示预处理的一部分(用上面的图2,从根结点0出发,深度搜索):

往上跳1步,等于父结点:grand[v][0] = nodelist[v].pre;

往上跳2^i步:grand[v][i] = grand[ grand[v][i - 1] ] [i - 1];   -----重点理解,自己试一遍

结点0:grand[0][0],根结点的父结点不存在。

结点2:grand[2][0]=0。

结点5:grand[5][0]=2; grand[5][1]=grand[grand[5][0]] [0]=grand[2][0]=0。

结点9:grand[9][0]=5; grand[9][1]=grand[grand[9][0]] [0]=grand[5][0]=2。

结点12:grand[12][0]=9; grand[12][1]=grand[grand[12][0]] [0]=grand[9][0]=5。

grand[12][2]=grand[grand[12][1]] [1]=grand[5][1]=0。

以此类推。

7、预处理好后,就可以用来找最近公共祖先。思路和步骤5类似,先让深度大的跳到和深度小的同深度,然后两个结点同时往上跳,只不过每次往上跳都采用倍增法,也就是预处理好的grand数组。因为用倍增法可能会跳过C,所以我才采用的方法是分别计算出A和B,A和C以及B和C的最近公共祖先,再判断C是否在AB之间。

四、实验过程原始数据记录

代码:

#include <iostream>
using namespace std;
#define MVnum 50010
#define Testnum 500000typedef struct VNode
{int data;  //结点的数据(小村的编号)VNode *next;int depth;  //结点所在树的深度信息int pre;    //父结点的编号
}VNode;
typedef struct Testdata
{int lval, rval, fval;  //测试数据的左右数值(A、B)和查找的数值(C)
}Testdata;int N, M; //N是结点(小村)的个数,M是测试问题的个数
VNode nodelist[MVnum];  //顶点表
Testdata testdata[Testnum]; //存储测试数据
/*-------------------------输入结点并创建邻接表---------------------------------*/
void create_udg()
{cout << "请输入结点个数:" << endl;cin >> N;for (int i = 0; i < N - 1; i++){nodelist[i].data = i;nodelist[i].next = NULL;   //初始化顶点}cout << "请输入" << N - 1 << "组相邻的两个结点:" << endl;int x, y;  //两个结点(小村)的编号for (int i = 0; i < N - 1; i++){cin >> x >> y;//x连接yVNode *p1 = new VNode;p1->data = y;p1->next = nodelist[x].next;nodelist[x].next = p1;//y连接xVNode *p2 = new VNode;p2->data = x;p2->next = nodelist[y].next;nodelist[y].next = p2;}
}/*-------------------------输入测试数据---------------------------------*/
void input_test()
{cout << "请输入测试数据组数:" << endl;int l, r, f;cin >> M;cout << "请输入" << M << "组测试数据:" << endl;for (int i = 0; i < M; i++)   //用结构体存储测试数据{cin >> r >> l >> f;testdata[i].rval = r;testdata[i].lval = l;testdata[i].fval = f;}
}/*-------------------------第一次深度优先搜索遍历--------------------------------*/
//统计信息,用于设置结点的深度,父结点的编号
int visited[MVnum]; //标志数组,初值为0
void dfs_f(int pre, int v, int depth)   //pre是父结点,v是顶点,d是深度
{// cout << v << endl;visited[v] = 1;nodelist[v].depth = depth;nodelist[v].pre = pre;VNode *p = nodelist[v].next;while (p != NULL){int w = p->data;if (!visited[w])    dfs_f(v, w, depth + 1);p = p->next;}
}/*---------------------------第二次深度优先搜索遍历--------------------------------*/
//预处理找最近公共祖先的数组(倍增法)
int grand[40001][20];   //第一个是结点,第二个是倍增的x,即2^x(2的x次方)
//最多能跳2^d个祖先
int find_jump(int depth)
{int high = 0;for (int d = 0; d < 30; d++){high = pow(2, d);if (high > depth) return d - 1;}
}
//第二次深度优先搜索遍历,预处理找最近公共祖先的数组grand(倍增法),时间复杂度为(O(nlogn))
int visited_2[MVnum];
void dfs_s(int v)
{visited_2[v] = 1;int depth = nodelist[v].depth;int n = find_jump(depth);    //找到最大但不超过根结点的2的次方数for (int i = 0; i <= n; i++){if (i == 0) {grand[v][0] = nodelist[v].pre;   //父结点,直接用之前遍历出来的}else {grand[v][i] = grand[grand[v][i - 1]][i - 1]; //倍增法,不断借用之前处理出来的}}VNode *p = nodelist[v].next;while (p != NULL){int w = p->data;if (!visited_2[w])    dfs_s(w);p = p->next;}
}/*-----------------------找最近公共祖先--------------------------------*/
//lac算法(用的是倍增法)找最近公共祖先
int lca(int l, int r)   //l是左结点编号,r是右结点编号
{if (nodelist[l].depth > nodelist[r].depth) {        //保持l在r的上面,便于计算int temp = l;l = r;r = temp;}/* 结果:l是深度小的,r是深度大的 */int ldepth = nodelist[l].depth;int rdepth = nodelist[r].depth;int rn = find_jump(rdepth);for (int i = rn; i >= 0; i--)        //把在下面的r跳到和l同深度{if (ldepth < nodelist[r].depth && nodelist[grand[r][i]].depth >= ldepth) {r = grand[r][i];}}//同层次后,两个结点一起往上跳int ln = find_jump(ldepth);for (int i = ln; i >= 0; i--){if (grand[l][i] != grand[r][i]) {    //可能会跳到最近公共祖先结点上面,相等但不满足l = grand[l][i];r = grand[r][i];}}int ans = l;    //情况出现在,最开始的l和r在同一支路上,通过上面跳到同一深度就直接找到if (l != r) {   //l不等于r,公共祖先就在上一深度ans = grand[l][0];}return ans;
}
//主函数
int main()
{create_udg();  //输入结点并创建邻接表input_test();   //输入测试数据//第一次深度遍历,第二个参数是遍历的根结点,第一个参数-1是根结点的父结点,不存在置为-1;//第三个参数是深度dfs_f(-1, 0, 0);//第二次深度遍历,参数是根结点0,用于预处理倍增法要用的数组grand[]dfs_s(0);int a, b, c;  //测试的三个数据int lca_ab, lca_ac, lca_bc;        //三个数据两两之间的最近祖先结点char *results[Testnum];    //存储最后的结果,yes或者nofor (int i = 0; i < M; i++)   //遍历所有测试数据(时间复杂度:O(M)){a = testdata[i].lval;b = testdata[i].rval;c = testdata[i].fval;lca_ab = lca(a, b);      //找最近公共祖先,时间复杂度(O(logn))lca_ac = lca(a, c);lca_bc = lca(b, c);if ((lca_ab == c) || ((lca_ac == c) && (lca_bc != c)) ||((lca_ac != c) && (lca_bc == c))) {results[i] = "yes";}else {results[i] = "no";}}//输出结果cout << "结果:" << endl;for (int i = 0; i < M; i++){cout << results[i] << endl;}return 0;
}

操作结果:

1、测试数据采用的是如下图所示的树,共6个结点。

  

在上图的树中,分析结果是否正确。当A=3,B=2,C=0时结果输出yes,在图中0确实在3和2的路径中间。当A=3,B=2,C=5时结果输出no,在图中5确实不在3和2的路径中间。所以测试数据都是正确的。

2、测试数据采用的是如下图所示的树,共13个结点。

下面开始测试,先输入N=13个结点,再依次输入树中两两相邻的边,接着输入M=4个测试数据,然后依次输入测试数据A、B、C,最后输出测试结果。

  

在上图的树中,分析结果是否正确。当A=1,B=3,C=0时结果输出yes,在图中0确实在1和3的路径中间。当A=12,B=6,C=2时结果输出yes,在图中2确实在12和6的路径中间。当A=12,B=6,C=7时结果输出no,在图中7确实不在12和6的路径中间。当A=4,B=5,C=8时结果输出no,在图中8确实不在4和5的路径中间。所以测试数据都是正确的。

五、实验结果及分析

实验结果:

1、测试数据,50000个结点和50000组测试数据,形成单链树的形式。

  

测试结果:

测试时间=总时间-读取文件时间;1.527s-0.916s=0.611s

因为总时间大部分是从文件读取数据的时间,所以要从总时间中减去文件读取数据的时间,才是处理数据的时间,可以看到结果,50000个结点形成的单链树,测试50000组数据的时间为0.611s。

2、测试最大数据,50000个结点和500000组测试数据,形成单链树的形式。

  

测试结果:

测试时间=总时间-读取文件时间;8.32s-5.535s=2.785s

因为总时间大部分是从文件读取数据的时间,所以要从总时间中减去文件读取数据的时间,才是处理数据的时间,可以看到结果,50000个结点形成的单链树,测试500000组数据的时间为2.785s。

实验分析:

  1. 在最开始处理这道题的时候,并没有想到好的算法来实现,所以采用的是用图的邻接表来存储这棵树的结点,然后在处理每组测试数据时都深度优先搜索遍历一次来查找C是否在A、B之间。所以时间复杂度很高,M组测试数据,每组测试数据最坏情况遍历全部结点N个,即M*N,时间复杂度为O(NM)。在测试大数据时,这个复杂度的算法处理效果很不理想。
  2. 接着采用了一些小方法来优化,一个是给每个结点加上深度,处理AB路径时只需往深度小的方向,也就是根结点方向走,所以并不需要遍历路径之外的结点,可以节省一些时间。然后考虑到两个结点往上走的路径重合后会重复遍历相同结点,另外也不需要遍历重复结点,所以采用了找最近公共祖先的方法。但是,考虑到极端情况下树变成了单链的形式,这两种优化方法都不能彻底解决问题,还是要一步步遍历,时间复杂度是O(N),N是结点个数,总时间复杂度就还是O(NM)。
  3. 最后采用的是结合上面的两个方法和倍增法来处理。倍增法是采用每次跳2的倍数来实现优化,在这之前要先预处理好用来倍增的数组,预处理的时间复杂度是O(NlogN)。处理好后在处理测试数据时,要找三个结点两两之间的最近公共祖先,每找一次最近公共祖先的时间复杂度是O(logN),三次即O(3logN)。另外有M组测试数据,所以最后的时间复杂度是O(3MlogN),也可以看做O(MlogN)。用来处理50000个结点和500000组数据大小效率基本没问题。

神秘国度的爱情故事--数据结构课程设计相关推荐

  1. 神秘国度的爱情故事——广州大学课程设计

    神秘国度的爱情故事 [问题描述] 某个太空神秘国度中有很多美丽的小村,从太空中可以想见,小村间有路相连,更精确一点说,任意两村之间有且仅有一条路径.小村 A 中有位年轻人爱上了自己村里的美丽姑娘.每天 ...

  2. 神秘国度的爱情故事 数据结构课设-广州大学

    神秘国度的爱情故事 数据结构课设-广州大学 ps:本次课设程序不仅需要解决问题,更需要注重代码和算法的优化和数据测试分析      直接广度优先实现的方法时间复杂度为O(QN),优化后的方法是lca+ ...

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

    数据结构 课程设计报告 广州大学 计算机科学与网络工程学院 计算机系 17级计科专业2班 2019年6月30日 广州大学学生实验报告 开课学院及实验室:计算机科学与工程实验室              ...

  4. 【广州大学】数据结构课程设计:神秘国度的爱情故事

    数据结构课程设计报告 广州大学 计算机科学与网络工程学院 计算机系 19级网络工程专业网络194班 超级菜狗 (学号:19062000) (班内序号:xxx) 完成时间:2021年1月11日 一.课程 ...

  5. 数据结构课程设计-神秘国度的爱情故事-LCA:tarjan+离线/树链剖分/暴力

    1.无脑暴力dfs:   O(n*m) 2.LCA/tarjan+离线处理: O(n+m) 3.LCA/树链剖分: O(nlogn+m)~O(nlogn+mlogn) 4.LCA/倍增思想(有空再补) ...

  6. 数据结构课程设计——机票售卖系统(C++)

    引言 这学期最后的数据结构课程设计需要我们完成一个简单的小程序,我选择了一个机票售卖系统,实现了一些基本的功能:因为时间给的比较短,又赶在复习周补课,所以并没有什么突出的地方,我就在这里聊聊我的代码实 ...

  7. 数据结构课程设计---最长公共子串

    数据结构课程设计,由用户输入两个字符串串X和Y,再由用户输入一个任意的字符串Z,实现以下功能: ①如果字符串Z是字符串X的子串,则显示Z在X中的位置并记录,如果字符串Z是字符串Y的子串,则显示Z在Y中 ...

  8. 设树采用孩子兄弟表示法存放.用类c语言设计算法计算树的高度.,(数据结构课程设计分类题目.doc...

    (数据结构课程设计分类题目 线性表 顺序表: 1.设有一元素为整数的线性表L=(a1,a2,a3,-,an),存放在一维数组A[N]中,设计一个算法,以表中an作为参考元素,将该表分为左.右两部分,其 ...

  9. c语言数据结构五子棋实验报告,数据结构课程设计-五子棋

    数据结构课程设计-五子棋 姓 名: 学 院: 计算机与通信学院 班 级: 通信工程 101 班 指导老师: 目录一.需求分析 31.1 开发背景 .32.2 功能简介 .3二.系统设计 42.1 函数 ...

最新文章

  1. 登陆模块防止恶意用户客户端攻击
  2. [USACO16JAN]子共七Subsequences Summing to Sevens
  3. 网站CSS样式不起作用,或只有一部分起作用?随手记
  4. HDOJ 4876 ZCC loves cards
  5. python turtle库输出文字_python turtle库学习笔记
  6. 设计模式:单件模式(Singleton Pattern)
  7. 简单-三层-存储过程-增删改《一》
  8. oracle sql 执行计划分析_ORACLE数据库查看执行计划
  9. zoj3829 Known Notation --- 2014 ACM-ICPC Asia Mudanjiang Regional Contest
  10. 微信翻译生日快乐的代码_新套路,微信这个翻译功能还能帮你表白,快学起来!...
  11. 怎样启动本地mysql服务_启动本地mysql服务
  12. 基本技能 100316
  13. 实录:记谷歌在微信脚下的一次翻车
  14. 查看源代码的几种方法
  15. 2023年,把“软件评测师”考起来吧~
  16. PPT如何保存高分辨率高清jpg图片, 三种简单方法,无需其他软件辅助
  17. 本地缓存、服务器缓存、分布式缓存介绍
  18. node.js学习-第一章节
  19. C++基础(持续更新)
  20. 东半球最佳的身份引擎服务,诚邀探索

热门文章

  1. SW2016提示cosworks.dll无法装入。
  2. H5页面在微信浏览器中打开,右上角没有出现三个点
  3. 支撑马蜂窝会员体系全面升级背后的架构设计
  4. R Markdown语法集锦
  5. 隆昌学电子计算机在什么地方,隆昌学种草莓的地方
  6. Java处理Excel方式对比
  7. 关于博客转型de说明
  8. C++ VS2017 mupdf 环境配置(入门篇)
  9. 磁学基础 永久磁铁的磁力线分布
  10. 关于mysql使用 判断null 和 空字符串