点击蓝字 关注我们

图论算法在计算机科学中扮演着很重要的角色,它提供了对很多问题都有效的一种简单而系统的建模方式。很多问题都可以转化为图论问题,然后用图论的基本算法加以解决。

图论算法是我们经常用来求解实际问题的一种方法,在数学建模的求解过程中也经常应用。

下面就通过一个例子,来让大家快速地知道什么是图,如下图所示:

G1 是有向图,G2 是无向图,每个数据元素称为顶点,在有向图中,从 V1 到 V3 称为一条弧,V3 到 V1 为另一条弧,V1 称为弧尾,V3 称为弧头,在无向图中,从 V1 到 V3 称为一条边。

(G1 有向图、G2 无向图)

有 n 个顶点,n(n-1)/2 条边的无向图称为完全图,有 n*(n-1)条弧有向图称为有向完全图,有很少条边或图称为稀疏图,反之称为稠密图

在 G2 无向图中,类似 V3 与 V1、V2 和 V4 之间有边的互称为邻接点,与顶点相关联的边数称为顶点的度。

例如 V3 顶点的度为 3,而在 G1 有向图中,顶点的度是顶点的出度和入度之和,以顶点为头的弧的数目称为入度,为尾的弧的数目称为出度,例如 V1 顶点的出度为 2,入度为 1,它的度为 1+2=3。

1.图的存储、遍历

图的常见存储方式有邻接矩阵存储、邻接表存储,又可扩展为十字链表存储、多重表存储。

  • 邻接矩阵存储

对于 N 个顶点的图,需创建一个 N 个空间的一维数组及 N*N 个空间的二维数组。一维数组负责存储每个顶点信息,二维数组则负责存储每条边的信息。

参考代码为:

class Graph:def __init__(self, vertex):self.vertex = vertexself.graph = [[0] * vertex for i in range(vertex)]def insert(self, u, v):# 对存在连接关系的两个点,在矩阵里置1代表存在连接关系,没有连接关系则置0self.graph[u - 1][v - 1] = 1self.graph[v - 1][u - 1] = 1def show(self):  # 展示图for i in self.graph:for j in i:print(j, end=' ')print(' ')graph = Graph(5)
graph.insert(1, 4)
graph.insert(4, 2)
graph.insert(4, 5)
graph.insert(2, 5)
graph.insert(5, 3)
graph.show()

该 graph 储存形式为:

0 0 0 1 0
0 0 0 1 1
0 0 0 0 1
1 1 0 0 1
0 1 1 1 0

  • 邻接表存储

对N个顶点的图,邻接表存储需要创建N个空间的数组,且每个空间都会存放一个链表节点(数据域与邻边节点指针)。

而邻边节点又存放了邻点下标和关于头节点的另一个邻边节点指针。整体看上去是一个数组,每个数组元素又是一个单链表。

参考代码为(以无向图为例):

class Graph(object):def __init__(self):self.nodes = []  # 表示图的点集self.edge = {}  # 表示图的边集def insert(self, a, b):# 如果 a 不在图的点集里,则添加 aif not(a in self.nodes):self.nodes.append(a)self.edge[a] = list()# 如果 b 不在图的点集里,则添加 bif not(b in self.nodes):self.nodes.append(b)self.edge[b] = list()# a 连接 bself.edge[a].append(b)# b 连接 aself.edge[b].append(a)def succ(self, a):# 返回与 a 连接的点return self.edge[a]def show_nodes(self):# 返回图的点集return self.nodesdef show_edge(self):print(self.edge)graph = Graph()
graph.insert('0', '1')
graph.insert('0', '2')
graph.insert('0', '3')
graph.insert('1', '3')
graph.insert('2', '3')
graph.show_edge()

该 graph 储存形式为:

{'0': ['1', '2', '3'], '1': ['0', '3'], '2': ['0', '3'], '3': ['0', '1', '2']}

图的遍历,又 bfs 广度优先遍历,dfs 深度优先遍历

其中广度优先遍历,类似于二叉树遍历中的层次遍历,是采用队列先把根节点旁边的都遍历一遍,在一层一层的往下扩展;而深度优先遍历,则像二叉树中的栈遍历,上去先走到最底部,发现没路了再往后退退,看见旁边有路了又往最深处钻。

参考代码如下:

def dfs(G,s,S=None,res=None):if S is None:# 储存已经访问节点S=set()if res is None:# 存储遍历顺序res=[]res.append(s)S.add(s)for u in G[s]:if u in S:continueS.add(u)dfs(G,u,S,res)return resG = {'0': ['1', '2'],'1': ['2', '3'],'2': ['3', '5'],'3': ['4'],'4': [],'5': []}print(dfs(G, '0'))

2.连通性问题

如果需求需要输出 N 个对象(整数)中,存在连接的数对。那么最多只能输出 N-1 个数对。如果能够输出 N-1 个数对,那说明给定的所有对象都是连通的。

1)快速查找算法

利用整数数组,每个整数对应一个对象,使用数组下标表示新建的数对。

#include <stdio.h>#define N (1000)int main()
{int i, p, q, t;int id[N];//自然数列,所有对象的数值互不相等,则表示大家之间都没有连接for (i = 0; i < N; i++) {id[i] = i;}//循环读入整数对while (scanf("%d-%d", &p, &q) == 2){//如果对象p与q是连通的,则读取下一对数对if (id[p] == id[q]) continue;//如果id[p]与id[q]的值不相等,则说明p-q是新对,就是没有连接//则将所有原本与id[p]元素值相等的所有元素连接到q,即建立连接for (t = id[p], i = 0; i < N; i++){if (id[i] == t)id[i] = id[q];}//因为p-q是新对,所以输出这个对printf("New pair: %d-%d\n", p, q);}return 0;}

2)快速合并算法

通过两个 for 循环,去查找数对 p 和 q 的根节点,并合并该节点。

#include <stdio.h>#define N (10)int main()
{int i, p, q, j;int id[N];//初始化对象集合中元素的初始值for (i = 0; i < N; i++) id[i] = i;//循环读入整数对while (scanf("%d-%d", &p, &q) == 2){//从位置P读取值,即读取p的根节点for (i = p; i != id[i]; i = id[i]);for (j = q; j != id[j]; j = id[j]);if (i == j){//i、j 位置对象的值相等则是已存在的连接//即p和q的根节点相同continue;}else{//不相等则说明是新连接id[i] = j;//因为p-q是新对,所以输出这个对printf("New pair: %d-%d\n", p, q);}}return 0;}

3.最短路

最短路径问题是图论研究中的一个经典算法问题,旨在寻找图(由结点和路径组成的)中两结点之间的最短路径。

给定一个图,和一个源顶点 src,找到从 src 到其它所有所有顶点的最短路径,图中可能含有负权值的边。

1)floyd

又称为插点法,是一种利用动态规划的思想寻找给定的加权图中多源点之间最短路径的算法。

它适用于 APSP(多源最短路径),是一种动态规划算法,稠密图效果最佳,边权可正可负。

此算法简单有效,由于三重循环结构紧凑,对于稠密图,效率要高于执行 |V| 次 Dijkstra 算法,也要高于执行 |V| 次 SPFA 算法。

  • 优点

容易理解,可以算出任意两个节点之间的最短距离,代码编写简单。

  • 缺点

时间复杂度比较高,不适合计算大量数据。

参考代码如下(以 C++ 为例):

#include<iostream>
#include<vector>
using namespace std;
const int &INF=100000000;
void floyd(vector<vector<int> > &distmap,//可被更新的邻接矩阵,更新后不能确定原有边vector<vector<int> > &path)//路径上到达该点的中转点
//福利:这个函数没有用除INF外的任何全局量,可以直接复制!
{const int &NODE=distmap.size();//用邻接矩阵的大小传递顶点个数,减少参数传递path.assign(NODE,vector<int>(NODE,-1));//初始化路径数组 for(int k=1; k!=NODE; ++k)//对于每一个中转点for(int i=0; i!=NODE; ++i)//枚举源点for(int j=0; j!=NODE; ++j)//枚举终点if(distmap[i][j]>distmap[i][k]+distmap[k][j])//不满足三角不等式{distmap[i][j]=distmap[i][k]+distmap[k][j];//更新path[i][j]=k;//记录路径}
}
void print(const int &beg,const int &end,const vector<vector<int> > &path)//传引用,避免拷贝,不占用内存空间//也可以用栈结构先进后出的特性来代替函数递归
{if(path[beg][end]>=0){print(beg,path[beg][end],path);print(path[beg][end],end,path);}else cout<<"->"<<end;
}
int main()
{int n_num,e_num,beg,end;//含义见下cout<<"(不处理负权回路)输入点数、边数:";cin>>n_num>>e_num;vector<vector<int> > path,distmap(n_num,vector<int>(n_num,INF));//默认初始化邻接矩阵for(int i=0,p,q; i!=e_num; ++i){cout<<"输入第"<<i+1<<"条边的起点、终点、长度(100000000代表无穷大,不联通):";cin>>p>>q;cin>>distmap[p][q];}floyd(distmap,path);cout<<"计算完毕,可以开始查询,请输入出发点和终点:";cin>>beg>>end;cout<<"最短距离为"<<distmap[beg][end]<<",打印路径:"<<beg;print(beg,end,path);
}

练习题指路→https://www.lanqiao.cn/problems/1121/learning/

2)SPFA

Bellman-Ford 算法的队列优化算法的别称,通常用于求含负权边的单源最短路径,以及判负权环。SPFA 最坏情况下复杂度和朴素 Bellman-Ford 相同,为 O(VE)。

参考代码如下(以 C++ 为例):

#include<iostream>
#include<vector>
#include<list>
using namespace std;
struct Edge
{int to,len;
};
bool spfa(const int &beg,//出发点const vector<list<Edge> > &adjlist,//邻接表,通过传引用避免拷贝vector<int> &dist,//出发点到各点的最短路径长度vector<int> &path)//路径上到达该点的前一个点
//没有负权回路返回0
//福利:这个函数没有调用任何全局变量,可以直接复制!
{const int INF=0x7FFFFFFF,NODE=adjlist.size();//用邻接表的大小传递顶点个数,减少参数传递dist.assign(NODE,INF);//初始化距离为无穷大path.assign(NODE,-1);//初始化路径为未知list<int> que(1,beg);//处理队列vector<int> cnt(NODE,0);//记录各点入队次数,用于判断负权回路vector<bool> flag(NODE,0);//标志数组,判断是否在队列中dist[beg]=0;//出发点到自身路径长度为0cnt[beg]=flag[beg]=1;//入队并开始计数while(!que.empty()){const int now=que.front();que.pop_front();flag[now]=0;//将当前处理的点出队for(list<Edge>::const_iterator//用常量迭代器遍历邻接表i=adjlist[now].begin(); i!=adjlist[now].end(); ++i)if(dist[i->to]>dist[now]+i->len)//不满足三角不等式{dist[i->to]=dist[now]+i->len;//更新path[i->to]=now;//记录路径if(!flag[i->to])//若未在处理队列中{if(NODE==++cnt[i->to])return 1;//计数后出现负权回路if(!que.empty()&&dist[i->to]<dist[que.front()])//队列非空且优于队首(SLF)que.push_front(i->to);//放在队首else que.push_back(i->to);//否则放在队尾flag[i->to]=1;//入队}}}return 0;
}
int main()
{int n_num,e_num,beg;//含义见下cout<<"输入点数、边数、出发点:";cin>>n_num>>e_num>>beg;vector<list<Edge> > adjlist(n_num,list<Edge>());//默认初始化邻接表for(int i=0,p; i!=e_num; ++i){Edge tmp;cout<<"输入第"<<i+1<<"条边的起点、终点、长度:";cin>>p>>tmp.to>>tmp.len;adjlist[p].push_back(tmp);}vector<int> dist,path;//用于接收最短路径长度及路径各点if(spfa(beg,adjlist,dist,path))cout<<"图中存在负权回路\n";else for(int i=0; i!=n_num; ++i){cout<<beg<<"到"<<i<<"的最短距离为"<<dist[i]<<",反向打印路径:";for(int w=i; path[w]>=0; w=path[w])cout<<w<<"<-";cout<<beg<<'\n';}
}

练习题指路→https://www.lanqiao.cn/problems/1366/learning/

3)Dijkstra

使用了广度优先搜索解决赋权有向图或者无向图的单源最短路径问题,算法最终得到一个最短路径树。该算法常用于路由算法或者作为其他图算法的一个子模块。

(PS:Dijkstra 算法不能处理包含负边的图!)

具体代码如下:

import heapq
def dijkstra(graph, start, end):heap = [(0, start)]  # cost from start node,end nodevisited = []while heap:(cost, u) = heapq.heappop(heap)if u in visited:continuevisited.append(u)if u == end:return costfor v, c in G[u]:if v in visited:continuenext = cost + cheapq.heappush(heap, (next, v))return (-1, -1)G = {'0': [['1', 2], ['2', 5]],'1': [['0', 2], ['3', 3], ['4', 1]],'2': [['0', 5], ['5', 3]],'3': [['1', 3]],'4': [['1', 1], ['5', 3]],'5': [['2', 3], ['4', 3]]}
shortDistance = dijkstra(G, '4', '2')
print(shortDistance)

练习题指路→https://www.lanqiao.cn/problems/1122/learning/

4.最小生成树

一个有 n 个结点的连通图的生成树是原图的极小连通子图,且包含原图中的所有 n 个结点,并且有保持图连通的最少的边。

最小生成树可以用 kruskal 算法或 prim 算法求出。

1)prim

由此算法搜索到的边子集所构成的树中,不但包括了连通图里的所有顶点,且其所有边的权值之和亦为最小。

具体代码如下(以 C++ 为例):

#define MAXN 1000
#define INF 1<<30
int closest[MAXN],lowcost[MAXN],m;//m为节点的个数
int G[MAXN][MAXN];//邻接矩阵
int prim()
{for(int i=0;i<m;i++){lowcost[i] = INF;}for(int i=0;i<m;i++){closest[i] = 0;}closest[0] = -1;//加入第一个点,-1表示该点在集合U中,否则在集合V中int num = 0,ans = 0,e = 0;//e为最新加入集合的点while (num < m-1)//加入m-1条边{int micost = INF,miedge = -1;for(int i=0;i<m;i++)if(closest[i] != -1){int temp = G[e][i];if(temp < lowcost[i]){lowcost[i] = temp;closest[i] = e;}if(lowcost[i] < micost)micost = lowcost[miedge=i];}ans += micost;closest[e = miedge] = -1;num++;}return ans;
}

练习题指路→https://www.lanqiao.cn/problems/1124/learning/

2)kruskal

求连通网的最小生成树的另一种方法。

与普里姆算法不同,它的时间复杂度为O(eloge)(e为网中的边数),所以,适合于求边稀疏的网的最小生成树

5.拓扑排序

对一个有向无环图 G 进行拓扑排序,是将 G 中所有顶点排成一个线性序列,使得图中任意一对顶点 u 和 v,若边 <u,v>∈E(G),则 u 在线性序列中出现在 v 之前。

通常,这样的线性序列称为满足拓扑次序的序列,简称拓扑序列。

简单的说,由某个集合上的一个偏序得到该集合上的一个全序,这个操作称之为拓扑排序。

具体代码如下(以 C++ 为例):

queue<int>q;
//priority_queue<int,vector<int>,greater<int>>q;
//优先队列的话,会按照数值大小有顺序的输出
//此处为了理解,暂时就用简单队列
int topo()
{for(inti=1;i<=n;i++){if(indegree[i]==0){q.push(i);}}int temp;while(!q.empty()){temp=q.front();//如果是优先队列,这里可以是top()printf("%d->",temp);q.pop();for(inti=1;i<=n;i++)//遍历从temp出发的每一条边,入度--{if(map[temp][i]){indegree[i]--;if(indegree[i]==0)q.push(i);}}}
}

练习题指路→https://www.lanqiao.cn/problems/1337/learning/

本周的【算法学与练】就结束啦~如果你想持续算法,欢迎加入专属算法刷题群~回复【算法】即可免费获取题解哦~

▼算法刷题群等你来▼

图论算法真的那么难吗?知识点都在这了……相关推荐

  1. hmm隐马尔可夫真的那么难吗?

    hmm隐马尔可夫真的那么难吗? 首先上代码 这里是github上的关于hmm的:链接 概率计算问题:前向-后向算法 学习问题:Baum-Welch算法(状态未知) 预测问题:Viterbi算法 htt ...

  2. 数据结构真的很难学?

    如果你关注计算机专业招聘试题,会发现越是大型公司,问的问题越基础,有的甚至问你什么是栈和队列,反而一些小公司会关心你做过什么系统.从关注点的不同可以看出,大公司更注重基础扎实和发展潜力,而小公司希望你 ...

  3. 图论算法——图的遍历

    图论算法也是非常基础且重要的算法(ps:好像没有不重要的......) 图的基本应用--图的遍历,从具体的题目着手,学习图的遍历方式及代码形式. 我们先来看一下题目,然后再具体分析图的遍历方式. 题目 ...

  4. 【liuyubobobo-玩转图论算法】第一章 课程概述

    持续学习&持续更新中- 守破离 [liuyubobobo-玩转图论算法]第一章 课程概述 图论概述 课程特色 课程大纲 图论的应用 参考 <玩转数据结构>是<图论课程> ...

  5. 将 Linux 移植到 M1 Mac 真的太难了!

    ????????关注后回复 "进群" ,拉你进程序员交流群???????? [CSDN 编者按]自去年苹果自研 M1 芯片发布之后,激发了无数用户的体验热情,与此同时,也吸引大批开 ...

  6. 死磕算法真的有必要吗?

    点击蓝字 关注我们 我们经常说:算法是编程的灵魂.无论是 Python,还是 PHP,都离不开算法.就连很多大厂在面试时,都会出算法题. 不过学习算法并不容易,且竞争压力较大,但这并不是说,你不会算法 ...

  7. 高中理科不好学计算机,理科非常差但我选了理 学理科真的很难吗

    其实在高一高二时我 的理科也很差,特别是物理,但高三一轮复习之后,经过大量的做题,就感觉自己突然开窍了.学习真的是个很神奇的东西,相信奇迹. 理科很烂选了理怎么办 你现在是读高二吧,马上就读高三了吧, ...

  8. 图论算法小结:网络流及其算法

    个人说明:最近学到了图论算法,但网络流这部分颇难理解,于是在网上找到了一片比较好的讲解博客.转载之~ 网络流(Network Flow) 将每条有向边想象成传输物质的管道.每个管道都有一个固定的容量, ...

  9. 机器学习与算法面试太难?

    机器学习与算法面试太难? 来源: https://mp.weixin.qq.com/s/GrkCvU2Ia_mEaQmiffLotQ 作者:石晓文 八月参加了一些提前批的面试,包括阿里.百度.头条.贝 ...

  10. 民营企业的项目,真的很难做

    民营企业的项目,真的很难做 笔者加入现在的项目超过2个月了.随着对于客户的了解越来越深入,尤其是通过合作的乙方咨询公司了解到一些情况,发现客户虽然越做越大越做越强了,但是却永远改变不了其民营企业的德性 ...

最新文章

  1. 活动 | 人工智能产学研生态建设研讨会报名开启
  2. 51Nod 1439 - 互质对(容斥+莫比乌斯函数)
  3. 参与Apache顶级开源项目的N种方式,Apache Dubbo Samples SIG 成立!
  4. 2016-05-09的POC Yaas Open Event的代码审查
  5. c语言成绩转换绩点,如何将平时成绩转化为GPA成绩?
  6. Android Handler的使用方法
  7. linux紧急救援模式,如何在 Ubuntu 18.04 中启动到救援模式或紧急模式
  8. Springboot 使用wangEditor3.0上传图片
  9. 74.android 简单的跳转到小米安全中心首页和小米安全中心的权限管理
  10. bellman operator 和bellman equation概念区分
  11. Shiro系统权限管理、及原理剖析
  12. RZ7886/7888/7889/7899/TA6586小功率直流电机正反转驱动芯片简介
  13. 比尔-盖茨2010年年信:世界首富的幸福观
  14. java web 添加超链接_[Java教程]javaWeb超链接(href)请求
  15. 美洽消息推送 php,GitHub - Meiqia/MeiqiaSDK-Push-Signature-Example: 美洽 SDK 3.0 推送的数据结构签名算法,多语言示例。...
  16. 如何启用服务器的TCP IP协议,本地联接的属性里的TCP/IP协议被禁用,怎么开启啊?...
  17. 天猫11.11:搜索引擎实时秒级更新
  18. 微信支付与登录之项目开发阶段1
  19. Blender学习笔记(基于辣椒酱教程)
  20. 一些linux的记录

热门文章

  1. 省市县地区编码五级联东2021年版sql
  2. win7计算机管理快捷键,win7系统快捷键有哪些|win7常用的15个快捷键
  3. 宇视摄像头IP地址修改工具
  4. 开源许可证 有人管吗_4个令人困惑的开源许可证场景以及如何浏览它们
  5. PR2018入门教程01-基础教程
  6. 自己动手写网络爬虫-----(1)
  7. 【数据艺术科技1】基于pyhon的高维数据可视化。(1、2维)
  8. 华为MAGICBOOK安装win10专业版,华为MAGICBOOK win10专业版X64下载
  9. instagram怎么用_用PHP和Instagram API征服Instagram
  10. Android 电视 文件目录,智能电视三款良心文件管理软件,操作攻略指南