算法导论之最小生成树(Kruskal和Prim算法)

一,最小生成树的概念
对于一个连通的无向图G=(V,E),每条边(u,v)∈E上赋予一个权重w(u,v)(可以是金钱、资源的消耗量或者长度等代价),我们需要找到一个E的无环子集T,既能将所有结点连接起来,又要让权重(代价)最小。由于T是无环的且连通所有结点,那他必然是棵树,我们称这棵树叫生成树,因为他是由图G生成的,而求最小权重的树的问题叫做最小生成树问题。

声明几个相关概念:
割(Cut): 如果无向图(u,v)∈E的一端点属于S,而另一个端点属于V-S时,则称(u,v)通过割(S,V-S)

轻边(Light edge): 如果某条边的权值是通过一个割的所有边中最小的,则称该边为通过这个割的一条轻边。

安全边(Safe edge): 如果一条边(u,v)能够加入集合A中而不违反循环不变式,则该边称为安全边。也即A∪(u,v)也是最小生成树的子集

二,最小生成树的形成
求最小生成树的方法有两种:Prime算法和Kruskal算法,两种算法都采用了贪心算法,即每一步都选择对于当前最佳的选择,这种贪心算法可以用下面这个通用的方法来描述,这个算法每一次让最小生成树增加一条边,并始终满足集合A中的边能形成最小生成树。

Kruskal算法
Kruskal算法的思想是,寻找连接森林中两棵不同树的边里面的最短边作为安全边加入集合A。可以使用不相交集合来维护这样的结构,对每个结点建立一棵树。通过FIND-SET来返回结点属于哪棵树,有边加入集合A时合并u和v所在的树。时间复杂度可表示为O(ElgV)。

伪代码:
在这里插入图片描述
过程图示:
在这里插入图片描述
python实现的代码:

# 构建无向图的类
class Graph(object):
    # 初始化函数
    def __init__(self, maps):
        # 传参 将邻接矩阵传入图中
        self.maps = maps
        # 获取结点数
        self.nodenum = self.get_nodenum()
        # 获取边数
        self.edgenum = self.get_edgenum()

    # 获取结点的数量
    def get_nodenum(self):
        return len(self.maps)

    # 遍历获取边数
    def get_edgenum(self):
        count = 0
        for i in range(self.nodenum):
            for j in range(i):
                if self.maps[i][j] > 0 and self.maps[i][j] < 9999:
                    count += 1
        return count

    # 定义克鲁斯卡尔算法
    def kruskal(self):
        # 定义res用来记录一条边的两个结点和其权重的列表
        res = []
        # 如果结点数小于零或者边数小于结点数减一  不满足条件
        if self.nodenum <= 0 or self.edgenum < self.nodenum - 1:
            # 直接返回空的res
            return res
        # 定义记录边的列表
        edge_list = []
        # 遍历与结点次数相同的次数
        for i in range(self.nodenum):
            # 遍历与结点次数相同的次数
            for j in range(i, self.nodenum):
                # 如果这条边存在
                if self.maps[i][j] < 9999:
                    # 将边存入列表中  按[begin, end, weight]形式加入
                    edge_list.append([i, j, self.maps[i][j]])
        # 给这些边按照权重排序
        edge_list.sort(key=lambda a: a[2])
        # 初始时构建与结点个数相同的树
        group = [[i] for i in range(self.nodenum)]
        # 遍历存储边的列表
        for edge in edge_list:
            # 遍历和树的个数相同的次数
            for i in range(len(group)):
                # 如果edge的一个结点在一棵树中
                if edge[0] in group[i]:
                    # 将结点包含i的树赋给m
                    m = i
                # 如果edge的另一个结点也在一颗树中
                if edge[1] in group[i]:
                    # 将结点包含i的树赋给n
                    n = i
            # 如果不在一棵树中
            if m != n:
                # 将挑选出的安全边加入到res
                res.append(edge)
                # 将包含结点为n的树加入到包含结点为m的树中
                group[m] = group[m] + group[n]
                # 并且将包含结点为n的树清空
                group[n] = []
        return res

# 定义最大结点数为9999
max_value = 9999
row0 = [0, 7, max_value, max_value, max_value, 5]
row1 = [7, 0, 9, max_value, 3, max_value]
row2 = [max_value, 9, 0, 6, max_value, max_value]
row3 = [max_value, max_value, 6, 0, 8, 10]
row4 = [max_value, 3, max_value, 8, 0, 4]
row5 = [5, max_value, max_value, 10, 4, 0]
# 定义六个结点
maps = [row0, row1, row2, row3, row4, row5]
# 实例化图并传参
graph = Graph(maps)
# 打印出邻接矩阵
print('邻接矩阵为\n%s' % graph.maps)
# 打印出结点数和边数
print('节点数据为%d,边数为%d\n' % (graph.nodenum, graph.edgenum))
# 调用克鲁斯卡尔算法  打印结果
print('------最小生成树kruskal算法------')
print(graph.kruskal())

Prim算法
Prim算法的思路是从根结点开始加入集合A,不断寻找A与V-A相连边中最短的,即横跨(A,V-A)的轻量级边。可以将V-A到A距离的最小值存储到优先队列Q中来减少每次遍历寻找最短边的时间。优先队列可以通过最小二叉堆或者斐波那契堆来实现,前者的渐进时间为O(ElgV)后者改进为O(E+VlgV)。

伪代码:
在这里插入图片描述
过程图示:
在这里插入图片描述
python实现的代码:

# 构建无向图的类
class Graph(object):
    # 初始化函数
    def __init__(self, maps):
        # 传参 将邻接矩阵传入图中
        self.maps = maps
        # 获取结点数
        self.nodenum = self.get_nodenum()
        # 获取边数
        self.edgenum = self.get_edgenum()

    # 获取结点的数量
    def get_nodenum(self):
        return len(self.maps)

    # 遍历获取边数
    def get_edgenum(self):
        count = 0
        for i in range(self.nodenum):
            for j in range(i):
                if self.maps[i][j] > 0 and self.maps[i][j] < 9999:
                    count += 1
        return count

    # 定义普里姆算法
    def prim(self):
        # 定义res用来记录一条边的两个结点和其权重的列表
        res = []
        # 如果结点数小于零或者边数小于结点数减一  不满足条件
        if self.nodenum <= 0 or self.edgenum < self.nodenum - 1:
            # 直接返回空的res
            return res
        # 再次清空res
        res = []
        # 直接选取根节点开始
        seleted_node = [0]
        # 将未被搜索到的结点单独放入一个列表
        candidate_node = [i for i in range(1, self.nodenum)]
        # 开始进入循环  当所有结点都被搜索到则退出循环
        while len(candidate_node) > 0:
            # 初始化begin和end均为0 和 最小权重minweight为无穷大(也就是9999) 
            begin, end, minweight = 0, 0, 9999
            # 遍历已被搜索到的结点
            for i in seleted_node:
                # 遍历未被搜索到的结点
                for j in candidate_node:
                    # 如果两个结点距离小于最小权重
                    if self.maps[i][j] < minweight:
                        # 将最小权重记录下来
                        minweight = self.maps[i][j]
                        # 记录一个顶点为begin
                        begin = i
                        # 另一个顶点为end
                        end = j
            # 将最终搜索到的权重最小的结点以及权重加入到res
            res.append([begin, end, minweight])
            # 将end也就是被搜索的结点加入到存储被搜索结点的列表中
            seleted_node.append(end)
            # 同时从存储未被搜索的结点中去除
            candidate_node.remove(end)
        # 当遍历完所有的结点后  返回结果 res
        return res

# 定义最大结点数为9999
max_value = 9999
row0 = [0, 7, max_value, max_value, max_value, 5]
row1 = [7, 0, 9, max_value, 3, max_value]
row2 = [max_value, 9, 0, 6, max_value, max_value]
row3 = [max_value, max_value, 6, 0, 8, 10]
row4 = [max_value, 3, max_value, 8, 0, 4]
row5 = [5, max_value, max_value, 10, 4, 0]
# 定义六个结点
maps = [row0, row1, row2, row3, row4, row5]
# 实例化图并传参
graph = Graph(maps)
# 打印出邻接矩阵
print('邻接矩阵为\n%s' % graph.maps)
# 打印出结点数和边数
print('节点数据为%d,边数为%d\n' % (graph.nodenum, graph.edgenum))
# 调用普拉姆算法  打印结果
print('------最小生成树prim算法')
print(graph.prim())
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值