【算法与图】通向高效解决方案的钥匙

【算法与图】通向高效解决方案的钥匙
在这里插入图片描述

文章目录

遍历算法

BFS(广度优先遍历)

1. 什么是 BFS?

BFS(广度优先搜索)是一种图的遍历算法,用于从一个起始节点出发,逐层访问图中的所有节点。其基本流程如下:

  1. 起始节点:选择一个节点作为起点。
  2. 队列:使用队列(FIFO)来保存待访问的节点。
  3. 访问过程
    • 将起始节点加入队列并标记为已访问。
    • 当队列不为空时:
      • 从队列中取出一个节点,访问该节点。
      • 将该节点的所有未访问邻居节点加入队列并标记为已访问。
  4. 层级遍历:BFS 会先访问距离起始节点最近的节点,然后逐层向外扩展,直到所有可以访问的节点都被访问。

2. 特点和应用

  • 最短路径:在无权图中,BFS 可以找到从起始节点到其他节点的最短路径。
  • 图的连通性:可以用来判断图的连通性,即判断两个节点是否在同一连通分量中。
  • 应用:广泛应用于网络流、社交网络分析、最短路径问题、迷宫求解等领域。

3. BFS 示例

假设我们有一个无向图,节点间的连接如下:

在这里插入图片描述


应该如何实现这种算法呢?
首先我们应该用一个队列来维护节点,进入BFS这个接口的时候,我们应该传入从哪个节点开始进行BFS。
然后用一个队列来维护节点,先将第一个节点push进队列中,然后将这个节点输出之后,先将这个节点保存起来然后再把这个节点pop掉,然后将与这个节点相连的节点push进队列(注意:这里可能会出现重复的节点,比如我们拿上面的例子为例,第一次push的时候我们将C节点已经push进队列了,但是第二层访问完了之后,到第三层的时候,B和C相连还会遍历一次C,所以这里我们应该用一个vector进行标记,标记这个节点被访问过没有),进入循环的条件是队列是否为空。
代码展示:

voidBFS(const V& src){size_t srci =GetVertexIndex(src); queue<int> q; q.push(srci);//队列 vector<bool>visited(_vertexs.size(), false);//标记数组 visited[srci]= true;int levelsize =1;size_t n = _vertexs.size();while(!q.empty()){for(int i =0;i < levelsize;i++){int front = q.front(); cout << front <<':'<< _vertexs[front]<<' '; q.pop();//把front顶点的邻接顶点入队列for(size_t i =0;i < _vertexs.size();i++){if(_matrix[front][i]!= MAX_W && visited[i]== false){ q.push(i); visited[i]= true;}}} cout << endl;//levelzie是下一层的数据个数 levelsize = q.size();}}

DFS(深度优先搜索)

1. 什么是 DFS?

DFS(深度优先搜索)是一种图的遍历算法,从起始节点开始,尽可能深入探索每个分支,直到无法再继续,然后回溯到上一个节点,继续探索其他分支。它适用于有向图和无向图。

2. DFS 的基本步骤

  1. 起始节点:选择一个节点作为起点。
  2. 深入探索:访问起始节点,并标记为已访问。
  3. 递归访问:对当前节点的每个未访问邻居节点递归进行深度优先搜索。
  4. 回溯:如果当前节点的所有邻居都被访问,则回溯到上一个节点,继续深度搜索其他分支。

3. 特点

  • 优先深入:DFS 尽可能先访问一个节点的最深分支,再逐步回溯到较浅的分支。
  • 非最短路径:与 BFS 不同,DFS 不保证找到最短路径,因为它按深度优先进行搜索。
  • 应用广泛:DFS 可用于检测图的连通性、拓扑排序、寻找路径和检测环。

4. DFS 的应用

  • 路径搜索:可以用于寻找图中从一个节点到另一个节点的路径。
  • 图的连通性:判断图中的连通分量。
  • 拓扑排序:用于有向无环图(DAG)的节点排序。
  • 检测环:可以检测图中是否存在环。

5. DFS 示例

在这里插入图片描述


DFS和BFS一样,还是给定一个起点,从这个起点开始进行,但是DFS的方式和BFS完全不同,DFS是一条路走到黑,从当前节点一直走,走到不能走为止,当走到不能走时,进行回溯,回溯到上一个岔口,然后向刚刚没有走过的路口继续走,走到尽头的时候又进行回溯,就一直这样递归,直到把所有节点遍历完为止。

代码展示:

void_DFS(size_t srci, vector<bool>& visited){//进来直接访问这个点 cout << srci <<':'<< _vertexs[srci]<< endl; visited[srci]= true;//找到下一个srci相邻的没有访问过的点,去往深度遍历for(size_t i =0;i < _vertexs.size();i++){//如果下一个节点没有被访问过直接遍历if(_matrix[srci][i]!= MAX_W && visited[i]== false){_DFS(i, visited);}}}voidDFS(const V& src){//获取起点下标size_t srci =GetVertexIndex(src); vector<bool>visited(_vertexs.size(), false);_DFS(srci, visited);}

注意:DFS中也需要用一个bool数组进行标记,当前位置是否被访问过。

最小生成树问题

1. 什么是最小生成树?

最小生成树是一个图的子集,包含图中的所有节点,并且是连通的,同时边的总权重最小。最小生成树的特点是没有回路,并且连接了图中的所有节点。

2. 最小生成树的基本特性

  • 包含所有节点:最小生成树包含图中的所有顶点。
  • 边的权重总和最小:在所有可能的生成树中,其边权重之和是最小的。
  • 无环图:最小生成树是一个无环的连通图。

3. 应用场景

  • 网络设计:如计算机网络、交通网络的最优连接。
  • 电路设计:用于布线问题,减少电缆长度。
  • 聚类分析:在数据科学中,用于分类和分组。

4. 最小生成树的算法

常用的求解最小生成树的算法有:

  1. Kruskal 算法
    • 通过选择边的方式逐步构建最小生成树,优先选择权重最小的边,确保不形成回路。
  2. Prim 算法
    • 从一个起始节点开始,逐步扩展生成树,选择连接已包含节点和未包含节点的最小权重边。

5. 最小生成树的图示:

在这里插入图片描述

下面的图就是上面的图的最小生成树的其中之一。最小生成树是不止一个的。如果我们选择上面那个8,最小生成树又不一样。

在这里插入图片描述

最小生成树算法

Kruskal算法

1. 什么是Kruskal算法?

克鲁斯卡尔算法是一种用于求解最小生成树(MST)的贪心算法。它通过选择边的方式逐步构建最小生成树,优先选择权重最小的边,并确保不形成回路。

2. 算法步骤

克鲁斯卡尔算法的基本步骤如下:

  1. 排序边:将图中的所有边按权重从小到大排序。
  2. 初始化:创建一个空的生成树,并初始化一个并查集(Union-Find)结构,用于检测图中的环。
  3. 选择边
    • 从权重最小的边开始,依次考虑每条边。
    • 对于每条边 (u, v),检查 uv 是否在同一连通分量中(使用并查集)。
    • 如果不在同一连通分量中,加入该边到生成树,并将 uv 的连通分量合并。
  4. 结束条件:当生成树中的边数等于 V-1V 为节点数)时,算法结束。

3. 算法复杂度

  • 时间复杂度O(E log E),其中 E 是边的数量,主要由排序边的时间决定。
  • 空间复杂度O(V),用于存储并查集。

4. 示例

在这里插入图片描述

5.思路及代码

根据克鲁斯卡尔算法的步骤:

  1. 排序边,我们可以用优先级队列来对边进行排序,但是这个边不能只有边,应该还需要边两端的顶点,所以我们需要把这个封装成一个结构体,也就是边集,还有一个问题就是排序,优先级队列的排序默认是从大到小,所以我们还需要写一个比较的逻辑进行比较。
  2. 初始化:初始化我们只需要将最小生成树初始化为原图的大小即可。大小和原图一模一样,顶点映射下标的关系也和原图一模一样。
  3. 选择边:在选择边时,我们只需要将存在优先级队列中的边取出来即可,但是在选择边时需要检查一下这个边加入之后是否会形成环,这时就可以利用并查集,因为并查集可以高效管理集合,我们开辟一个并查集的大小是顶点个数的并查集,如果这条边选了就将这两个顶点的集合合并,如果会形成环的话,那么对应的两个顶点肯定在一个集合当中。所以我们只需要判断这两个顶点是否在一个集合当中即可,如果没在一个集合当中,就将这条边给最小生成树,并且将这两个顶点的集合合并,还需要一个累加权值的变量记录总的权值大小。
  4. 结束条件:用size记录边数的大小,当边的条数等于顶点的个数-1的时候就是结束的时候。

代码展示:

W Kruskal(Self& minTree){size_t n = _vertexs.size(); minTree._vertexs = _vertexs; minTree._indexMap = _indexMap; minTree._matrix.resize(n);for(auto& e : minTree._matrix) e.resize(n, MAX_W);//优先级队列,优先级默认是大的优先级高,所以这里要控制 priority_queue<Edge, vector<Edge>, greater<Edge>> minq;for(int i =0;i < n;i++){for(int j =0;j < n;j++){//i<j保证只访问一次if(i < j && _matrix[i][j]!= MAX_W);{//将edge插入进去,由于无向图会出现重复添加的情况,所以无向图走一半即可 minq.push(Edge(i, j, _matrix[i][j]));}}}//选出n-1条边//开辟一个n个顶点的并查集size_t size =0;//总的权值 W totalW =W(); UnionFindset ufs(n);while(!minq.empty()){//选择一条边 Edge eg = minq.top(); minq.pop();//观察两个顶点是否在同一个集合if(!ufs.IsInSet(eg._srci, eg._dsti)){ cout << _vertexs[eg._dsti]<<"->"<< _vertexs[eg._srci]<<':'<< eg._w << endl;//将边添加到mintree中 minTree._AddEdge(eg._srci, eg._dsti, eg._w);//将顶点添加到并查集当中 ufs.Union(eg._srci, eg._dsti); totalW += eg._w;++size;}}if(size == n -1)return totalW;elsereturnW();}

Prim算法

1. 什么是Prim算法?

普利姆算法是一种用于求解最小生成树(MST)的贪心算法。它从一个节点开始,通过逐步选择连接已访问节点和未访问节点的最小权重边来扩展生成树,直到所有节点都被包含。

2. 算法步骤

普利姆算法的基本步骤如下:

  1. 选择起始节点:从图中的任意一个节点开始(通常是第一个节点)。
  2. 初始化:将起始节点加入生成树,并将它的所有邻边放入一个优先队列(最小堆),按边的权重排序。
  3. 选择最小权重边:从优先队列中取出权重最小的边,并检查其连接的节点是否已在生成树中。
    • 如果该节点已在生成树中,忽略这条边。
    • 如果该节点不在生成树中,将该节点和边加入生成树,并将新节点的所有邻边加入优先队列。
  4. 重复:不断从优先队列中选取最小权重边,直到生成树包含所有节点。

3. 算法复杂度

  • 时间复杂度O(E log V),其中 E 是边的数量,V 是节点数量,主要由最小堆操作决定。
  • 空间复杂度O(V^2)

4. 示例

在这里插入图片描述

5.思想及代码

普林姆算法和克里姆林算法还是有很大的差别的,普林姆算法需要起始点,然后将起始点相连最小的边入到边集当中。

在这里插入图片描述


假设这个图我们以a点为例,和a相连的边是b和h点,ab边明显小于ah边,所以我们选择ab边

在这里插入图片描述


这里引入了b点,所以我们的选择是bc边,bh边还有ah边,很明显这里我们选择bc边也可以,ah边也可以,所以我们就选择ah边吧。

在这里插入图片描述


这里我们可以选择的边种最小的是hg边。

在这里插入图片描述


接下来我们肯定选择gf,因为gf最小

在这里插入图片描述


最后可以推出最小生成树

在这里插入图片描述


Prim算法的思路很简单,实现过程我们还是用优先级队列,但是在选择的时候我们需要判断一下是否形成环。
代码展示:

W Prim(Self& minTree,const V& src){//初始化size_t srci =GetVertexIndex(src);size_t n = _vertexs.size(); minTree._vertexs = _vertexs; minTree._indexMap = _indexMap; minTree._matrix.resize(n);for(auto& e : minTree._matrix)e.resize(n, MAX_W);//选过的顶点 vector<bool>X(n, false); vector<bool>Y(n, true); X[srci]= true, Y[srci]= false;//从X->Y集合中连接的边里面选出最小的边 priority_queue<Edge, vector<Edge>, greater<Edge>> minq;for(size_t i =0;i < _vertexs.size();i++){//把srci连接的边添加到队列中if(_matrix[srci][i]!= MAX_W)minq.push(Edge(srci, i, _matrix[srci][i]));} cout <<"Prim开始选边"<< endl;size_t size =0; W totalW =W();while(!minq.empty()){ Edge min = minq.top(); minq.pop();//如果最小边的目标点也在起点集合if(X[min._dsti]){ cout <<"构成环:"; cout << _vertexs[min._srci]<<"->"<< _vertexs[min._dsti]<<":"<< min._w << endl;}else{ minTree._AddEdge(min._srci, min._dsti, min._w); cout << _vertexs[min._srci]<<"->"<< _vertexs[min._dsti]<<":"<< min._w << endl; size++; totalW += min._w;if(size == n -1)break;//将dsti添加到X集合 X[min._dsti]= true; Y[min._dsti]= false;for(size_t i =0;i < _vertexs.size();i++){//将目标点连接的边添加到集合当中并且需要判断目标点是否已经在集合当中if(_matrix[min._dsti][i]!= MAX_W && X[i]== false)minq.push(Edge(min._dsti, i, _matrix[min._dsti][i]));}}}if(size == n -1)return totalW;elsereturnW();}

结尾总结

通过本文的讲解,我们深入了解了图论中的三种经典算法:广度优先搜索(BFS)、深度优先搜索(DFS)和最小生成树(Kruskal算法和Prim算法)。这些算法在计算机科学、数据分析、人工智能等领域有着广泛的应用。

从简单的图遍历到复杂的网络优化问题,这些算法都展现了其强大的解决问题能力。然而,图论是一个庞大的领域,还有许多更深入、更复杂的算法等待我们去探索。例如,拓扑排序、强连通分量、最短路径问题等。

希望本文能为你打开图论算法的大门,激发你对算法学习的兴趣。在未来的学习中,我们可以继续深入研究图论算法,并将其应用到实际的项目中。

Read more

服务器宕机前预警!Uptime Kuma+cpolar 让监控随时随地

服务器宕机前预警!Uptime Kuma+cpolar 让监控随时随地

文章目录 * 前言 * 一、前期准备 * 本教程环境为:Centos7,可以跑Docker的系统都可以使用本教程安装。 * 本教程使用Docker部署服务,如何安装Docker详见: * 二、Docker部署Uptime Kuma * 三、实现公网查看网站监控 * 四、使用固定公网地址访问本地部署的监控服务 * **Uptime Kuma 的轻量监控能力,搭配 cpolar 的远程访问支持,共同构成了一套零成本的全栈监控方案。它既能满足中小团队对服务器、网站的实时监测需求,又能打破地域限制,让运维工作不再受限于局域网,实现高效、灵活的远程管理。** 前言 Uptime Kuma 是一款轻量级的开源监控工具,支持 HTTP、TCP、Ping 等多种协议,能实时监测网站、服务器、API 接口的运行状态,还能通过邮件、钉钉等数十种方式推送告警信息。它适合个人开发者、中小企业运维人员使用,优点是占用资源少(单实例内存低于 100MB)、操作简单、

By Ne0inhk

vscode remote ssh 记住账号密码

在使用 VS Code 的 Remote - SSH 扩展时,每次连接远程服务器都需要手动输入账号和密码可能会比较麻烦。为了记住账号和密码,可以使用以下几种方法: 方法 1:使用 SSH 密钥认证 SSH 密钥认证是最安全且方便的方式,可以避免每次输入密码。 1. 生成 SSH 密钥对 在本地机器上生成 SSH 密钥对(如果尚未生成): bash ssh-keygen -t rsa -b 4096 -C "[email protected]" * 按提示操作,可以选择默认路径保存密钥(~/.ssh/id_rsa 和 ~/.ssh/id_rsa.pub)

By Ne0inhk

【Vibe Coding系列】解决 Ubuntu 安装Codex CLI 和 IDE 插件时报“Token exchange failed: 403 Forbidden” 问题

在 Ubuntu 22.04 上部署 OpenAI Codex CLI 时,常见的失败点并不集中在“包本身是否可用”,而是落在更基础的运行时与鉴权链路上:Node.js 版本不满足最低要求、全局安装目录权限不足,以及在 CLI 与 IDE(以 Cursor 为代表)中通过 ChatGPT 账号登录时出现 403 Forbidden 的 token 兑换失败。本文给出一条可复现的排障路径:先修复安装环境,再解释 403 的成因与规避方式,最后通过设备码登录稳定打通 CLI 与 IDE 插件的共享认证状态。 一、推荐安装方式:使用 nvm 安装 Node LTS,再安装 Codex

By Ne0inhk
ARM Linux 驱动开发篇--- Linux 并发与竞争实验(信号量实现 LED 设备互斥访问)--- Ubuntu20.04信号量实验

ARM Linux 驱动开发篇--- Linux 并发与竞争实验(信号量实现 LED 设备互斥访问)--- Ubuntu20.04信号量实验

🎬 渡水无言:个人主页渡水无言 ❄专栏传送门: 《linux专栏》《嵌入式linux驱动开发》《linux系统移植专栏》 ❄专栏传送门: 《freertos专栏》《STM32 HAL库专栏》 ⭐️流水不争先,争的是滔滔不绝  📚博主简介:第二十届中国研究生电子设计竞赛全国二等奖 |国家奖学金 | 省级三好学生 | 省级优秀毕业生获得者 | ZEEKLOG新星杯TOP18 | 半导纵横专栏博主 | 211在读研究生 在这里主要分享自己学习的linux嵌入式领域知识;有分享错误或者不足的地方欢迎大佬指导,也欢迎各位大佬互相三连 目录 前言 一、实验基础说明 1.1、信号量简介 1.2 本次实验设计思路 二、硬件原理分析(看过之前博客的可以忽略) 三、实验程序编写 3.1 信号量 LED 驱动代码(spinlock.c) 3.2、驱动代码分段解析 3.2.

By Ne0inhk