小话数据结构-图 (聚焦于案例实现的理解)

2023-11-27 22:30

本文主要是介绍小话数据结构-图 (聚焦于案例实现的理解),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

数学使我们能够发现概念和联系这些概念的规律,这些概念和规律给了我们理解自然现象的钥匙。

​ ——爱因斯坦

前言

本文代码基于C++实现,阅读本文,需要有以下知识

  1. 教熟练使用C++ STL库中的vector,map,pair等;
  2. 对于递归和简单搜索算法(dfs,bfs)有粗浅的理解;
  3. 稍微的离散数学或者是线性代数知识(可能是我瞎掰的,没有也罢 😂 )

本文针对算法或数据结构初学者~~(比如我)~~写下,本人不才,如有错误请轻喷 :happy: 。

瓶颈

在学习**“数据结构”这门课之前,“图论”这个略显高级的词汇看起来还与我那么的遥远,在经过了“离散数学”的学习之后,我慢慢认识到其实数据结构就是离散数学模型的代码实现**,并且在不断的学习中,我开始能以我自己的思维去理解图论的知识。

数据结构的教科书上,每个知识点都会系统的从**“定义——术语——存储结构——相关操作”**娓娓道来,然而,各种各样的障碍阻碍着我们认知的过程。对于离散数学不熟悉的,一时间无法抽象出模型,教材上冗长的代码实现,给读者一种晦涩难懂的感觉。这时候我们就要思考离散数学的本质——将具体问题抽象为一般问题,在由算法解决。因此,我们在一开始不必过于在意方法,而是应该聚焦与实现,像大学物理做实验一样,多操作几遍,自然就熟能生巧,甚至开发出新的理解。

你得从用户的体验出发,推倒用什么技术。
——乔布斯在1997年回归苹果发布会上回答提问

案例-引入

什么是图?简单说就是点与点之间的网状关系。

比如以下有6个城市之间的铁路线。

img

地图就是一个很经典的图

那么,我们是如何表达他们的关系的呢

我们使用邻接表or邻接矩阵

为什么是邻接表/矩阵

邻接表和邻接矩阵是图的两种常用存储表示方式,用于记录图中任意两个顶点之间的连通关系,包括权值。

(在图论相关的案例中,我们会特别频繁的用到这两种表示方式)

我们不谈什么二元组三元组的,想想啊,图重要的顶多就仨玩意:节点,边,权值,而使用邻接表和邻接矩阵已经可以清晰简洁的表达这些关系了。

我们先看看邻接表和邻接矩阵怎么建;

邻接表

emm,用图表示就是上面那张图啊,代码实现的话,我们用stl的vector更方便一点

#include <iostream>
#include <vector>struct edge{//边信息,边有啥属性往里丢就行int to;//该边可以去往的下一个节点编号,有明确指向,是单向边。int value;//存边的权值,如果没有可以直接忽略edge(int t,int v) :to(t), value(v){};//构造函数
};vector<edge> node[n];//用vector实现邻接表的存储
//ps:边信息只有一个可以直接存int,两个可以用stl的pair,三个及以上就肯定要用struct了
//vector<int> node[n],vector<pair<int, int> >都是可行的,自己清楚意思就行

这里是什么意思呢,就是我有n个vector数组,每一个数组表示一个点的信息,vector里存的是边(edge),每一个点到点的通路意味着有一个对应的edge信息

那么,如何把信息存入邻接表呢,我们假设用一下流程输入(大概就是平时做题的时候题目要求的输出啦)

先假设每条边的权值都是一样的,就设为“1”吧

//第一行为两个整数n,e,分别表示节点数,边数,随后n行,输入节点信息,在随后e行,接受边信息

6 8

武汉 岳阳 南昌 长沙 株洲 湘潭

武汉 岳阳

岳阳 南昌

武汉 长沙

南昌 长沙

岳阳 长沙

长沙 株洲

长沙 湘潭

湘潭 株洲

#EOF

那么,在C语言里面我们这么处理输入

map<string,int> tab;
map<int,string> tab0;
//建立散列表(哈希表),使每个城市的编号和名字可以相互连接
int n,m;
cin>>n>>m;
for(int i=0;i<n;i++){string name;cin>>name;tab0.insert({i,name});tab.insert({name,i});//给每一个城市编号
}
while(m--){edge tmp;string a,b;node[map[a]].push_back(edge(map[b],1));//构造函数中map[a]表示名为a的城市对应的编号,1表示权值,存入vector数组g中node[map[b]].push_back(edge(map[a],1));//因为是双向边所以正向反向都存一遍
}

这样的话,我们的邻接表就完全存储好了

对于任意一个点,我们要遍历其相邻点,只需要用一下代码

//输入城市名字 输出其相邻所有城市的名字string name;
cin>>name;
for(auto it=node[tab[name]].begin();it!=g[tab[name]].begin();it++){//遍历name节点的所有边cout<<tab0[it->to]<<endl;
}

假如我们输入

岳阳

那么就会返回

武汉

南昌

长沙

整个邻接表的存储和访问过程就是以上的样子了

邻接矩阵

这个就很好理解了,就是一个n*n的二维数组模拟矩阵,表达的是点与点之间的关系,我们沿用上一个例子里的输入,我们建立出来的矩阵大概是这样

img

这个邻接矩阵只是判定有无直接相连的,我们用一个6*6的二维数组可以很轻松的建出来,没有自旋,未联通和自我比较设置为0(false),已联通即设置为1(true)。

案例-常见问题

(PS:本人才疏学浅,只介绍部分案例的大致思维路线,细节欢迎各位深入思考)

我们接着上一个案例看,对于很多情况,邻接矩阵像上面这样,就算建出来了,但是我们现在用的,是一个实实在在的生活中的例子,谁都直到武汉和南昌之间必定可以通过铁路线到达,只是会经过别的站台。

这个时候就引出了一个问题,按照邻接表来看,武汉和南昌实际上是通过其他的节点连接起来了的,只是没有直接连接

然而此时,从“武汉“到”南昌”实际上有多条线路

武汉->岳阳->南昌

武汉->长沙->南昌

武汉->长沙->岳阳->南昌

武汉->岳阳->长沙->南昌

武汉->长沙->湘潭->株洲->南昌

………………

那我们给其付的权值到底是2,3,4还是多少呢?

这就可以引入到一个**常见的图论问题——“最短路”**了

最短路

(PS:最短路问题在算法竞赛和数学建模竞赛中都是非常常见的)

故名思意,当我们想要直接去往某个目的地时,一定是讲究时间效率的,我们不愿意走太长,更不愿意绕圈子,用规范的话说就是:“找最短路,并且避免系统资源浪费”,那我们就要先走一遍所有路径,看看哪个路径可行(好比每天高铁第一班车是“探路车”)。

假设我们要从武汉出发,去南昌:

我们从武汉开始遍历武汉接下来可以到达每一个城市

img

如此以来,逐个分析每个为直接相连的点,我们可以得到整个图的带权值的的邻接矩阵表示,其中有一个要点,即点不能重复访问,而BFS按照层次遍历邻接表的模式非常契合这个目的。我们沿用上方的输入和邻接表的存储形式,以下给出大致的伪代码:

#include<queue>//stl的queue容器void bfs(int st){queue<edge> qu;//创建队列qu.push(起始状态入队);while(!qu.empty()){//当队列非空if(当前状态x方向可走)qu.push(当前状态->x);//该状态入队if(当前状态向y方向可走)qu.push(当前状态->y);//该状态入队…………………处理(队顶)qu.top();相应操作;qu.pop();//队首弹出队}//一次循环结束,执行下一次循环
}

img

如此以来,我们就得到了整个图,每个节点的详细信息,可以根据需求进行更细节的操作。

这便是最短路的基本思想,当然,实际情况会更加复杂,比如边的权值各有不同,是有向边,出现负权值等情况,也会有相应的算法(迪特斯科拉,贝尔曼-福德,弗洛伊德,SPFA,A_Star等算法),同时,在图的遍历时,通过邻接矩阵我们也可以瞥见连通图的很多性质,优美的现象能吸引人的思考,数学之美就在于这些奇妙之处。

欧拉路

我们有目的性出行,肯定也有旅游出行,肯定有人喜欢欣赏沿途的风景,我举个例子,加入有一个岳阳人,他很喜欢看火车沿路的风景,他把以上6个城市作为了自己旅行可规划的目的地,他想在各个路线中穿梭,路线越长越好,但是他不喜欢看重复的风景,他想规划一个走过的路不重复,而且最长的路线。走过的路不重复,就是所谓的欧拉路

因为我们着重考虑路径,所以使用之前判定有无直接连接的邻接矩阵就可以了,我们使用DFS来遍历所有边并找出最长的一条。

int g[N][N];//邻接矩阵2维数组
//默认邻接矩阵信息已经存入了该二维数组中
bool st[N][N];//标记某一条边是否被访问过
int ans;//存储答案void dfs(int start, int res) {for (int i = 0; i < n; i++) {if (g[start][i] == 1 || st[start][i] || st[i][start]) //该边可以通过并且是第一次通过continue;st[u][i] = st[i][u] = true;//标记dfs(i, res + 1);//下一步st[u][i] = st[i][u] = false;//回溯}ans = max(ans, res);//保证得到最大的结果
}

欧拉(回)路问题其实就是经典的“一笔画问题”,应为我们每一步的判定和操作都是固定的,通过dfs的**“自相性”**我们往往能简洁而优美的解决这一系列问题。

为了继续思考图论的模型,我们接下来不使用代码讨论另外两种模型,这两种模型的代码模板很方便理解,了解了基本思路和模型,就很方便应用了。

并查集

我们都知道每个省有很多地放,现在随意给你两个市区的名称,想要你判断以下他们是否属于同一个省份。

我们将每个城市存入其数据结构,可以得出以下的情况

img

并查集的关键在于处理父子节点的关系,这样的数据架构可以处理大量的**“集合合并”**操作

最小生成树

再来一个例子,假设我们要再部分城市之间架设最新最快的交通轨道,为了使成本最低,现在要你选出一个方案,使架设的轨道线路最低:(现在我们给边附上权值)

img

这样便是一个基础的无向图最小生成树问题,我们根据我们已经建立的关系,采用并查集的数据结构,采用Kruskal或者Prim算法的模板可以求出以下结果

img

更多……

图论的问题和每一个节点的信息息息相关,而如何使用图论模型,关键在于如何定义**“节点”**。

关于这个问题,我很喜欢**《算法图解》**里的讲解方式——将“状态”转化为“信息”储存到“节点”里,每个状态是一个“节点”,状态变化的过程就是“边”。这便是连接实际问题和图论算法的桥梁,理解了这个思想,很多模型建立的困惑就能迎刃而解了,图论的其他问题基本都能通过这个思想来建模。

希望我的抛砖引玉能引起更多的思考! :happy: (蒟蒻鞠躬)

这篇关于小话数据结构-图 (聚焦于案例实现的理解)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/428565

相关文章

Flutter实现文字镂空效果的详细步骤

《Flutter实现文字镂空效果的详细步骤》:本文主要介绍如何使用Flutter实现文字镂空效果,包括创建基础应用结构、实现自定义绘制器、构建UI界面以及实现颜色选择按钮等步骤,并详细解析了混合模... 目录引言实现原理开始实现步骤1:创建基础应用结构步骤2:创建主屏幕步骤3:实现自定义绘制器步骤4:构建U

SpringBoot中四种AOP实战应用场景及代码实现

《SpringBoot中四种AOP实战应用场景及代码实现》面向切面编程(AOP)是Spring框架的核心功能之一,它通过预编译和运行期动态代理实现程序功能的统一维护,在SpringBoot应用中,AO... 目录引言场景一:日志记录与性能监控业务需求实现方案使用示例扩展:MDC实现请求跟踪场景二:权限控制与

Android实现定时任务的几种方式汇总(附源码)

《Android实现定时任务的几种方式汇总(附源码)》在Android应用中,定时任务(ScheduledTask)的需求几乎无处不在:从定时刷新数据、定时备份、定时推送通知,到夜间静默下载、循环执行... 目录一、项目介绍1. 背景与意义二、相关基础知识与系统约束三、方案一:Handler.postDel

使用Python实现IP地址和端口状态检测与监控

《使用Python实现IP地址和端口状态检测与监控》在网络运维和服务器管理中,IP地址和端口的可用性监控是保障业务连续性的基础需求,本文将带你用Python从零打造一个高可用IP监控系统,感兴趣的小伙... 目录概述:为什么需要IP监控系统使用步骤说明1. 环境准备2. 系统部署3. 核心功能配置系统效果展

Python实现微信自动锁定工具

《Python实现微信自动锁定工具》在数字化办公时代,微信已成为职场沟通的重要工具,但临时离开时忘记锁屏可能导致敏感信息泄露,下面我们就来看看如何使用Python打造一个微信自动锁定工具吧... 目录引言:当微信隐私遇到自动化守护效果展示核心功能全景图技术亮点深度解析1. 无操作检测引擎2. 微信路径智能获

Python中pywin32 常用窗口操作的实现

《Python中pywin32常用窗口操作的实现》本文主要介绍了Python中pywin32常用窗口操作的实现,pywin32主要的作用是供Python开发者快速调用WindowsAPI的一个... 目录获取窗口句柄获取最前端窗口句柄获取指定坐标处的窗口根据窗口的完整标题匹配获取句柄根据窗口的类别匹配获取句

在 Spring Boot 中实现异常处理最佳实践

《在SpringBoot中实现异常处理最佳实践》本文介绍如何在SpringBoot中实现异常处理,涵盖核心概念、实现方法、与先前查询的集成、性能分析、常见问题和最佳实践,感兴趣的朋友一起看看吧... 目录一、Spring Boot 异常处理的背景与核心概念1.1 为什么需要异常处理?1.2 Spring B

Python位移操作和位运算的实现示例

《Python位移操作和位运算的实现示例》本文主要介绍了Python位移操作和位运算的实现示例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一... 目录1. 位移操作1.1 左移操作 (<<)1.2 右移操作 (>>)注意事项:2. 位运算2.1

如何在 Spring Boot 中实现 FreeMarker 模板

《如何在SpringBoot中实现FreeMarker模板》FreeMarker是一种功能强大、轻量级的模板引擎,用于在Java应用中生成动态文本输出(如HTML、XML、邮件内容等),本文... 目录什么是 FreeMarker 模板?在 Spring Boot 中实现 FreeMarker 模板1. 环

Qt实现网络数据解析的方法总结

《Qt实现网络数据解析的方法总结》在Qt中解析网络数据通常涉及接收原始字节流,并将其转换为有意义的应用层数据,这篇文章为大家介绍了详细步骤和示例,感兴趣的小伙伴可以了解下... 目录1. 网络数据接收2. 缓冲区管理(处理粘包/拆包)3. 常见数据格式解析3.1 jsON解析3.2 XML解析3.3 自定义