【学习笔记】Matlab和python双语言的学习(最小生成树——Kruskal算法、Prim算法)


前言

通过模型算法,熟练对Matlab和python的应用。
学习视频链接:
https://www.bilibili.com/video/BV1EK41187QF?p=38&spm_id_from=pageDriver&vd_source=67471d3a1b4f517b7a7964093e62f7e6

一、最小生成树

树的一些概念

  • 连通图(Connected Graph):指无向图中任意两个顶点之间都有路径相连。也就是说,在一个连通图中,任意两个顶点之间都可以通过图中的边相连而相互到达。
    在这里插入图片描述

  • 树(Tree):一种无向无环连通图,它由 n n n 个节点和 n − 1 n-1 n1 条边组成。树具有很多重要的性质,比如任意两个节点之间都有唯一的路径、树中任意两个节点之间的路径长度唯一等等。

  • 生成树(Spanning Tree):指一个连通图的一棵包含所有顶点的树,它是由原图的所有顶点和边所组成的子图,且这些边构成一个树。一个图,可能有多个生成树。
    在这里插入图片描述

  • 最小生成树(Minimum Spanning Tree,简称MST)是一个连通的无向图的子图,它包含了图中所有的节点,并且边的总权重最小

关键特性

  1. 连通性:最小生成树包含图中所有的节点,并且节点之间是连通的。
  2. 无环性:最小生成树中没有环(环:从一点出发经过若干条边又回到该点的路径)。
  3. 边数:对于一个包含 ( V ) 个节点的图,最小生成树包含 ( V-1 ) 条边。
  4. 最小权重:在所有可能的生成树中,最小生成树的边的权重之和最小。

最小生成树和最短路径的主要区别

  • 最小生成树(MST):在一个无向连通图中,找到一棵树,使得它包含图中的所有节点,并且边的总权重最小。MST是关于图的全局结构的优化问题。
  • 最短路径(Shortest Path):在图中寻找两个节点之间的路径,使得路径上的边的总权重最小。最短路径是关于图的局部结构的优化问题。
  • MST:目的是连接所有节点,形成一个无环的子图,并使得边的总权重最小。
  • 最短路径:目的是找到特定两个节点之间的权重最小的路径。

常用算法

有两种主要的算法用于寻找最小生成树:Kruskal算法和Prim算法。

1. Kruskal算法(适合点多边少的图)

Kruskal算法是一种贪心算法,它通过逐步选择权重最小的边,并确保不形成环,最终构建出最小生成树。

步骤

  1. 把图 G G G 中所有边全部去掉,得到所有单独的顶点 V V V 构成的图 T T T

  2. 从图 G G G 中取出当前权值最小的边,如果该边加入 T T T 的边集合后 T T T 不形成回路,则加入 T T T,否则舍弃

  3. 重复第二步,直到 T T T 中有 n − 1 n-1 n1 条边( n n n 是定点数)

    第二步若遇到两条权值相同的最小权值边,任选一条即可,所以最小生成树可能不唯一

2. Prim算法(适合边多点少的图)

Prim算法也是一种贪心算法,它以一个节点为起点,逐步扩展生成树,每次选择与生成树相连的最小权重边。

步骤

  1. 设一空图 U U U,首先将图 G G G 中任意一顶点取出加入 U U U

  2. 从图 U U U 外并与图 U U U 连接的边中,找到权值最小的边和该边连接的顶点并入图 U U U

  3. 重复第二步,直到 U U U 中包含了所有顶点

    第二步若遇到两条权值相同的最小权值边,任选一条即可,所以最小生成树可能不唯一

二、示例

  • A国有六个城市之间一直没有通信线路,现在国家想使这六个城市通信连通
  • 六个城市,相互之间能够建设通信线缆的线路路径距离已测
  • 要求以最小的成本建设通信线路,使得这六个城市之间能够互相通信
  • 从任意顶点出发,都可以到达其他顶点,且线缆总长度最小
    在这里插入图片描述

根据要求:

  1. 从任意顶点出发,都可以到达其他顶点
  2. 线缆总长度最小

判断出该示例应使用最小生成树算法

三、代码实现----Matlab

clc,clear
% matlab中,不存在的边设置成0
% 6个顶点,初始化定义6x6的全零矩阵作为邻接矩阵
a = zeros(6);

% 注意,最小生成树是针对无向图的,每条边权重只需要设一次。1到2和2到1是同一条边
% 因此,可仅使用邻接矩阵的上三角矩阵来构造图G
a(1,[2 3])=[14 18];       % 顶点1到其他顶点的边的权重
a(2,[3:5])=[13 18 16];   % 顶点2到顶点3、顶点9的边的权重
a(3,[4 5])=[12 16];       % 同上。因为写过1到3,和2到3的边的权重,无需重复设
a(4,[5 6])=[14 19];       
a(5,6)=10;

s=cellstr(strcat('城市',int2str([1:6]')));
G=graph(a,s,'upper');  % 仅使用 A 的上三角矩阵来构造图G。
p=plot(G,'EdgeLabel',G.Edges.Weight);   % 绘制出图G

% minspantree函数求解最小生成树
% T=minspantree(G)是默认使用Prim算法
T=minspantree(G,'Method','sparse');      % 可指定使用Kruskal算法

L = sum(T.Edges.Weight)     % 对最小生成树的边的权重求和
highlight(p,T,"EdgeColor",'r','LineWidth',2.5)

运行结果:

在这里插入图片描述
在这里插入图片描述

四、代码实现----python

1. Kruskal算法

(1)表示出示例图

graph = {
    '1': {'2': 14, '3': 18},
    '2': {'1': 14, '3': 13, '4': 18, '5': 16},
    '3': {'1': 18, '2': 13, '4': 12, '5': 16},
    '4': {'2': 18, '3': 12, '5': 14, '6': 19},
    '5': {'2': 16, '3': 16, '4': 14, '6': 10},
    '6': {'4': 19, '5': 10}
}

(2)初始化节点

parent = {vertex: vertex for vertex in graph.keys()}

(3)获取边的两个节点和边的权重

# 获取所有的边
edges = []

for vertex, neighbors in graph.items():
    for neighbor, weight in neighbors.items():
        edges.append((weight,vertex, neighbor))

# 将 edges 转换为堆
heapq.heapify(edges)

(4)定义并查集函数

需要使用并查集判断一条边加入后是否形成环

并查集的相关概念见:https://blog.csdn.net/m0_65032457/article/details/141224025?spm=1001.2014.3001.5501

def find(parent, vertex):
    if parent[vertex] != vertex:
        parent[vertex] = find(parent, parent[vertex])  # 路径压缩
    return parent[vertex]

def union(parent, vertex1, vertex2):
    # 查找两个节点的根
    root1 = find(parent, vertex1)
    root2 = find(parent, vertex2)
 
    if root1 != root2:
        parent[root2] = root1

(5)使用 heapq.heappop() 函数弹出最短边

若加入该边不形成环,则使用 heapq.heappush() 函数将该边加入最小生成树。

heapq 的概念见:https://blog.csdn.net/m0_65032457/article/details/141135370?spm=1001.2014.3001.5501

while edges:
    weight, note1, note2 = heapq.heappop(edges)     
    if find(parent, note1) != find(parent, note2):
        union(parent, note1, note2)
        heapq.heappush(res, (weight, note1, note2))

Kruskal算法的完整代码

import heapq

def find(parent, vertex):
    if parent[vertex] != vertex:
        parent[vertex] = find(parent, parent[vertex])  # 路径压缩
    return parent[vertex]

def union(parent, vertex1, vertex2):
    # 查找两个节点的根
    root1 = find(parent, vertex1)
    root2 = find(parent, vertex2)
 
    if root1 != root2:
        parent[root2] = root1

def kruskal_algorithm(graph):

    # 初始化并查集
    parent = {vertex: vertex for vertex in graph.keys()}

    # 获取所有的边
    edges = []

    # 初始化一个空的堆用来存放最小生成树的结果
    res = []

    for vertex, neighbors in graph.items():
        for neighbor, weight in neighbors.items():
            edges.append((weight,vertex, neighbor))

    # 将 edges 转换为堆
    heapq.heapify(edges)

    while edges:
        weight, note1, note2 = heapq.heappop(edges)     
        if find(parent, note1) != find(parent, note2):
            union(parent, note1, note2)
            heapq.heappush(res, (weight, note1, note2))
    
    return res

graph = {
    '1': {'2': 14, '3': 18},
    '2': {'1': 14, '3': 13, '4': 18, '5': 16},
    '3': {'1': 18, '2': 13, '4': 12, '5': 16},
    '4': {'2': 18, '3': 12, '5': 14, '6': 19},
    '5': {'2': 16, '3': 16, '4': 14, '6': 10},
    '6': {'4': 19, '5': 10}
}

result = kruskal_algorithm(graph)
print(result)

运行结果:

在这里插入图片描述
数据分别代表:边的权重、节点1、节点2

2. Prim算法

算法思路:

  1. 任意选取图中的一个节点为一个独立的树
  2. 取出除这棵树外,所有与这棵树相连的节点到这棵树的距离,加入优先队列
  3. 弹出优先队列中距离这棵树最近的节点,并将其加入这棵树(更新这棵树的范围)
  4. 重复2、3步骤,直到遍历完所有的节点

代码思路:

  1. 算法角度:在 Prim 算法中,需要确定 “树” 的范围,以便找到除这棵树外,所有与这棵树相连的节点。

    代码角度:创建一个集合 visited = set() ,用来存放已访问的节点(树范围内的节点都存放于集合中),通过节点是否在集合内判断出节点是否在树中。

  2. 算法角度:找到树范围外,所有与树相连的节点

    代码角度:树范围外 ——> 节点不在集合 visited 内,与树相连 ——> 相连的节点在集合 visited 内。(通过这两个条件找出了与树相连的节点和到这棵树的距离,生成了优先队列)

完整代码思路:

(1)表示出示例图

graph = {
    '1': {'2': 14, '3': 18},
    '2': {'1': 14, '3': 13, '4': 18, '5': 16},
    '3': {'1': 18, '2': 13, '4': 12, '5': 16},
    '4': {'2': 18, '3': 12, '5': 14, '6': 19},
    '5': {'2': 16, '3': 16, '4': 14, '6': 10},
    '6': {'4': 19, '5': 10}
}

(2)初始化

# 选取 '1' 作为一个独立的树,权重是 0,前驱节点是None
priority_queue = [(0,'1',None)]

# 初始化集合,用来存放已访问的节点
visited = set()

# 存放最小生成树
res = []

(3)生成优先队列

# 更新相邻节点的距离
for neighbor, distance in graph[note1].items():
    if neighbor not in visited:   # 与节点 1 相邻的节点 1' 是否在树中
        for n, d in graph[neighbor].items():    # 取出该相邻结点 1' 的相邻结点 2'
            if n in visited:     # 相邻结点 2'在树中,代表相邻的节点 1'与树相连
                heapq.heappush(priority_queue,(d,neighbor,n))   # 加入优先队列

(4)更新树的范围

# 弹出堆中距离最小的节点
weight, note1, note2 = heapq.heappop(priority_queue)
priority_queue = []  # 清空优先队列 

if note2 != None:
    visited.add(note2)  # 将前驱节点加入已访问中 (更新树的范围)  
    res.append((weight, note1, note2))  # 更新最小生成树

visited.add(note1)  # 将该节点也加入已访问中 (更新树的范围)  

Prim算法的完整代码

import heapq

def prim_algorithm(graph):
    
    # 选取 '1' 作为一个独立的树,权重是 0,前驱节点是None
    priority_queue = [(0,'1',None)]

    # 初始化集合,用来存放已访问的节点
    visited = set()

    # 存放最小生成树
    res = []

    while priority_queue:

        # 弹出堆中距离最小的节点
        weight, note1, note2 = heapq.heappop(priority_queue)
        # print("距离最小的节点是:",weight, note1, note2)
        priority_queue = []  # 清空优先队列 

        if note2 != None:
            visited.add(note2)  # 将前驱节点加入已访问中   
            res.append((weight, note1, note2))
            # print("最小生成树进度:",res)

        visited.add(note1)  # 将该节点也加入已访问中
        # print("已访问的节点:",visited)

        # 更新相邻节点的距离
        for neighbor, distance in graph[note1].items():
            if neighbor not in visited:   # 与节点 1 相邻的节点 1' 是否在树中
                for n, d in graph[neighbor].items():    # 取出该相邻结点 1' 的相邻结点 2'
                    if n in visited:     # 相邻结点 2'在树中,代表相邻的节点 1'与树相连
                        heapq.heappush(priority_queue,(d,neighbor,n))   # 加入优先队列
                        # print("优先队列:",priority_queue)
    return res

graph = {
    '1': {'2': 14, '3': 18},
    '2': {'1': 14, '3': 13, '4': 18, '5': 16},
    '3': {'1': 18, '2': 13, '4': 12, '5': 16},
    '4': {'2': 18, '3': 12, '5': 14, '6': 19},
    '5': {'2': 16, '3': 16, '4': 14, '6': 10},
    '6': {'4': 19, '5': 10}
}

result = prim_algorithm(graph)
print(result)

运行结果:

在这里插入图片描述

总结

本文介绍了求解最小生成树的两种算法—— Kruskal和Prim,详细介绍了两种算法的求解思路。分别使用matlab和python进行了算法的实现。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值