目录
- 1 图的基本概念
- 2 如何做图
- 2.1 直接做图
- 2.2 编程做图
- 3 权重邻接矩阵
- 3.1 无向图
- 3.2 有向图
- 4 Dijkstra 算法
- 4.1 算法概述
- 4.2 代码实现
- 5 Floyd 算法
- 5.1 算法概述
- 5.2 代码实现
- 6 思考题
1 图的基本概念
图论中的图(Graph)是由若干给定的点及连接两点的线所构成的图形,这种图形通常用来描述某些事物之间的某种特定关系,用点代表事物,用连接两点的线表示相应两个事物间具有这种关系。
一个图可以用数学语言描述为:G(V(G),E(G))
。V(vertex)
指的是图的顶点集,E(edge)
指的是图的边集。
根据边是否有方向,可将图分为无向图和有向图。另外,有些图的边上还可能有权值,这样的图称为有权图。
2 如何做图
2.1 直接做图
在线做图工具的网址
2.2 编程做图
无向图
graph(s,t)
:可在s
和t
中的对应节点之间创建边,并生成一个图graph(s,t,w)
:可在s
和t
中的对应节点之间以w
的权重创建边,并生成一个图
要做出有向图,只需要将 graph
改为 digraph
就行了。
1️⃣ 无向图
% 无权重,也可以说每条边的权重默认为1
s1 = [1,2,3,4];
t1 = [2,3,1,1];
% 函数graph(s,t):可在 s 和 t 中的对应节点之间创建边,并生成一个图
% s 和 t 都必须具有相同的元素数;这些节点必须都是从1开始的正整数,或都是字符串元胞数组。
G1 = graph(s1, t1);
plot(G1)
% 注意字符串元胞数组是用大括号包起来的哦
s2 = {'学校','电影院','网吧','酒店'};
t2 = {'电影院','酒店','酒店','KTV'};
G2 = graph(s2, t2);
plot(G2, 'linewidth', 2) % 设置线的宽度
% 下面的命令是在画图后不显示坐标
set( gca, 'XTick', [], 'YTick', [] );
% 有权重
s = [1,2,3,4];
t = [2,3,1,1];
w = [3,8,9,2];
% 函数graph(s,t,w):可在 s 和 t 中的对应节点之间以w的权重创建边,并生成一个图
G = graph(s, t, w);
plot(G, 'EdgeLabel', G.Edges.Weight, 'linewidth', 2)
set( gca, 'XTick', [], 'YTick', [] );
❗️注意:
- 注意哦,编号只能从1开始连续编号(否则会报错),不要自己随便定义编号,因为默认连续编号,不写也有
s = [1,2,3,50];
t = [2,3,1,1];
G = graph(s, t);
plot(G)
2️⃣ 有向图
% 无权图 digraph(s,t)
s = [1,2,3,4,1];
t = [2,3,1,1,4];
G1 = digraph(s, t);
plot(G1)
set( gca, 'XTick', [], 'YTick', [] );
% 有权图 digraph(s,t,w)
s = [1,2,3,4];
t = [2,3,1,1];
w = [3,8,9,2];
G2 = digraph(s, t, w);
plot(G2, 'EdgeLabel', G.Edges.Weight, 'linewidth', 2)
set( gca, 'XTick', [], 'YTick', [] );
☀️ 总结:
- Matlab做出来的图不是很漂亮,要是节点比较少,还是推荐使用在线
3 权重邻接矩阵
3.1 无向图
3.2 有向图
4 Dijkstra 算法
4.1 算法概述
图中有 0~8 共九个地点,地点之间若用直线连接,则表明两地可直接到达,直线旁的数值表示两地的距离。
问起点为0,终点为4,怎么走路程最短?
使用 Dijkstra
算法解决上述问题
1️⃣ 初始化
Visited
:所有的节点都是未访问的状态;
Distance
:所有节点间的距离都是 Inf
;
Parent
:所有节点的父节点(上一个节点)都是 -1,表示不存在
1️⃣ 起点是0,更新表格:
- 节点0的访问状态变为1
- 节点0对应的距离变为0
- 节点0的父节点用0表示,当然用其他符号表示都可以
2️⃣ 更新与节点0(A)相邻节点(B)的信息,注意,这里的B节点是未访问的
- 如果A与B的距离 + A的距离小于B的距离,那么我们就将B的距离更新为较小的距离,并将B的父节点更新为A,并建行较小距离的节点纳入访问过的节点中
3️⃣ 更新与节点1(A)相邻节点(B)的信息,注意,这里的B节点是未访问的
- 如果A与B的距离 + A的距离小于B的距离,那么我们就将B的距离更新为较小的距离,并将B的父节点更新为A,并建行较小距离的节点纳入访问过的节点中
4️⃣ 重复上述步骤,最终得到:
根据以上结果,我们可以得到节点0到节点4的最短路径:
Dijkstra
算法一般用于无向图求最短路径,也可以用于有向图,但是Dijkstra
算法的一个缺点即不能用于处理有负权重的图
Bellman‐Ford
算法
为了解决Dijkstra
算法不能用于处理有负权重图的缺点,提出了 Bellman‐Ford
算法
事实上,Bellman‐Ford
算法不再将节点区分为是否已访问的状态,因为Bellman‐Ford
算法是利用循环来进行更新权重的,且每循环一次,Bellman‐Ford
算法都会更新所有的节点的信息。
Bellman‐Ford
算法不支持含有负权回路的图(Floyd算法也不可以)
4.2 代码实现
[P,d] = shortestpath(G,start,end [,'Method',algorithm])
-
功能:返回图
G
中start
节点到end
节点的最短路径 -
输入参数:
G
: 输入图(graph
对象 或digraph
对象)start
:起始的节点end
:目标的节点[,‘Method’,algorithm]
:是可选的参数,表示计算最短路径的算法。一般我们不用手动设置,默认使用的是auto
-
输出参数:
P
最短路径经过的节点d
最短距离
示例代码:
%% 注意:以下代码需要较新版本的matlab才能运行(最好是2016版本及以上)
% 如果运行出错请下载新版的matlab代码再运行
% 注意哦,Matlab中的图节点要从1开始编号,所以这里把0全部改为了9
% 编号最好是从1开始连续编号,不要自己随便定义编号
s = [9 9 1 1 2 2 2 7 7 6 6 5 5 4];
t = [1 7 7 2 8 3 5 8 6 8 5 3 4 3];
w = [4 8 3 8 2 7 4 1 6 6 2 14 10 9];
G = graph(s,t,w);
plot(G, 'EdgeLabel', G.Edges.Weight, 'linewidth', 2)
set( gca, 'XTick', [], 'YTick', [] );
[P,d] = shortestpath(G, 9, 4) %注意:该函数matlab2015b之后才有哦
% 在图中高亮我们的最短路径
myplot = plot(G, 'EdgeLabel', G.Edges.Weight, 'linewidth', 2); %首先将图赋给一个变量
highlight(myplot, P, 'EdgeColor', 'r') %对这个变量即我们刚刚绘制的图形进行高亮处理(给边加上r红色)
% 求出任意两点的最短路径矩阵
D = distances(G) %注意:该函数matlab2015b之后才有
D(1,2) % 1 -> 2的最短路径
D(9,4) % 9 -> 4的最短路径
% 找出给定范围内的所有点 nearest(G,s,d)
% 返回图形 G 中与节点 s 的距离在 d 之内的所有节点
[nodeIDs,dist] = nearest(G, 2, 10) %注意:该函数matlab2016a之后才有
输出图形:
5 Floyd 算法
5.1 算法概述
Floyd
算法是解决任意两点间的最短路径的一种算法,可以正确处理无向图或有向图(可以有负权重,但不可存在负权回路)的最短路径问题。
Floyd
算法与 Dijkstra
算法或Bellman‐Ford
算法相比,能够一次性的求出任意两点之间的最短路径,后两种算法运行一次只能计算出给定的起点和终点之间的最短路径。当然,Floyd
算法计算的时间也要高于后两种算法,其算法核心的步骤由三层循环构成。
算法动画展示
从上面观察到的两个结论中,我们不难提炼出下面这个思想:
- 假设现在有一个起点A和终点B,那么对于其他任意的中间点M:
D(A,B) ≤ D(A,M) + D(M,B)
,这里,D(X,Y)表示X和Y两点之间的最短距离。
因此,Floyd算法实际上核心在于一个三层循环
5.2 代码实现
☀️ 实现计算任意两点之间的最短路径的距离
☀️ 求最短路径(记录最短路径经过的点)
将伪代码转换为 Matlab 代码:
1️⃣ 定义Floyd
算法的函数Floyd_algorithm.m
function [dist,path] = Floyd_algorithm(D)
%% 该函数用于求解一个权重邻接矩阵任意两个节点之间的最短路径
% 输入:
% D是权重邻接矩阵
% 输出:
% dist是最短距离矩阵,其元素dist_ij表示表示i,j两个节点的最短距离
% path是路径矩阵,其元素path_ij表示起点为i,终点为j的两个节点之间的最短路径要经过的节点
n = size(D,1); % 计算节点的个数
% 初始化dist矩阵
dist = D;
% 下面我们来初始化path矩阵
path = zeros(n);
for j = 1:n
path(:,j) = j; % 将第j列的元素变为j
end
for i = 1:n
path(i,i) = -1; % 将主对角线元素变为-1
end
% 下面开始三个循环
for k=1:n % 中间节点k从1- n 循环
for i=1:n % 起始节点i从1- n 循环
for j=1:n % 终点节点j从1-n 循环
if dist(i,j)>dist(i,k)+dist(k,j) % 如果i,j两个节点间的最短距离大于i和k的最短距离+k和j的最短距离
dist(i,j)=dist(i,k)+dist(k,j); % 那么我们就令这两个较短的距离之和取代i,j两点之间的最短距离
path(i,j)=path(i,k); % 起点为i,终点为j的两个节点之间的最短路径要经过的节点更新为path(i,k)
% 注意,上面一行语句不能写成path(i,j) = k
end
end
end
end
end
2️⃣ 定义打印任意两节点间的最短路径的函数 print_path.m
function [] = print_path(path,dist,i,j)
%% 该函数的作用是打印从i到j经过的最短路径
% 输入:
% path是使用floyd算法求出来的路径矩阵
% dist是使用floyd算法求出来的最短距离矩阵
% i是起始节点的编号
% j是终点节点的编号
% 输出:无
if i == j
warning('起点和终点相同,请检查后重新输入') % 在屏幕中提示警告信息
return; % 不运行下面的语句,直接退出函数
end
if path(i,j) == j % 如果path(i,j) = j,则有两种可能:
% (1)如果dist(i,j) 为 Inf , 则说明从i到j没有路径可以到达
if dist(i,j) == Inf
disp(['从',num2str(i),'到',num2str(j),'没有路径可以到达'])
% (2)如果dist(i,j) 不为 Inf , 则说明从i到j可直接到达,且为最短路径
else
disp(['从',num2str(i),'到',num2str(j),'的最短路径为'])
disp([num2str(i),' ---> ',num2str(j)])
disp(['最短距离为',num2str(dist(i,j))])
end
else % 如果path(i,j) ~= j,则说明中间经过了其他节点:
k = path(i,j);
result = [num2str(i),' ---> ']; % 初始化要打印的这个字符串
while k ~= j % 只要k不等于j, 就一直循环下去
result = [result , num2str(k) , ' ---> ' ]; % i先走到k这个节点处
k = path(k,j);
end
result = [result , num2str(k)];
disp(['从',num2str(i),'到',num2str(j),'的最短路径为'])
disp(result)
disp(['最短距离为',num2str(dist(i,j))])
end
end
3️⃣ 定义打印所有的任意两节点间的最短路径的函数 print_all_path.m
function [] = print_all_path(D)
%% 该函数的作用是求解一个权重邻接矩阵任意两个节点之间的最短路径,并打印所有的结果出来
% 输入:
% D是权重邻接矩阵
% 输出:无
[dist,path] = Floyd_algorithm(D); % 调用之前的Floyd_algorithm函数
n = size(D,1);
if n == 1
warning('请输入至少两阶以上的权重邻接矩阵') % 在屏幕中提示警告信息
return; % 不运行下面的语句,直接退出函数
end
for i = 1:n
for j = 1:n
if i ~= j % 不等号用~=表示
print_path(path,dist,i,j); % 调用之前的print_path函数
disp('-------------------------------------------')
disp(' ')
end
end
end
end
4️⃣ 将图转换为权重邻接矩阵D,并调用 Floyd
算法的函数
%% 首先将图转换为权重邻接矩阵D
n = 5; %一共五个节点
D = ones(n) ./ zeros(n); % 全部元素初始化为Inf【有向图】
for i = 1:n
D(i,i) = 0; % 主对角线元素为0
end
D(1,2) = 3;
D(1,3) = 8;
D(1,5) = -4;
D(2,5) = 7;
D(2,4) = 1;
D(3,2) = 4;
D(4,3) = -5;
D(5,4) = 6;
D(4,1) = 2;
%% 调用Floyd_algorithm函数求解
[dist,path] = Floyd_algorithm(D)
print_path(path,dist,1,5)
print_path(path,dist,1,4)
print_path(path,dist,3,1)
clc
disp('下面我们打印任意两点之间的最短距离:')
print_all_path(D)
6 思考题
求出任意两点间的最短路径
参考答案:
%% 首先将图转换为权重邻接矩阵D
n = 9; %一共9个节点
D = zeros(n); % 全部元素初始化为0 【无向图】
% 因为是无向图,所以权重邻接矩阵是一个对称矩阵
D(1,2) = 4; D(1,8) = 8;
D(2,8) = 3; D(2,3) = 8;
D(8,9) = 1; D(8,7) = 6;
D(9,7) = 6; D(9,3) = 2;
D(7,6) = 2; D(3,4) = 7;
D(3,6) = 4; D(6,4) = 14;
D(4,5) = 9; D(6,5) = 10;
D = D+D'; % 这个操作可以得到对称矩阵的另一半
for i = 1:n
for j = 1:n
if (i ~= j) && (D(i,j) == 0)
D(i,j) = Inf; % 将非主对角线上的0元素全部变为Inf
end
end
end
%% 调用Floyd_algorithm函数求解
[dist,path] = Floyd_algorithm(D)