Python数据结构与算法——第四十八课 图的最小生成树Kruskal算法

  克鲁斯卡尔(Kruskal)算法查找最小生成树的方法是:将连通图中所有的边按照权值大小做升序排序,从权值最小的边开始选择,只要此边不和已选择的边一起构成环路,就可以选择它组成最小生成树。对于 N 个顶点的连通网,挑选出 N-1 条符合条件的边,这些边组成的生成树就是最小生成树。

  图解下面的连通图的最小生成树。

 

  克鲁斯卡尔(Kruskal)算法查找上图对应的最小生成树,需要经历以下几个步骤:
  (1) 将连通网中的所有边按照权值大小做升序排序:

 

  (2) B-D 边开始挑选,由于尚未选择任何边组成最小生成树,且 B-D 自身不会构成环路,所以 B-D 边可以组成最小生成树。

 

  (3) D-T 边不会和已选 B-D 边构成环路,可以组成最小生成树:

 

  (4) A-C 边不会和已选 B-DD-T 边构成环路,可以组成最小生成树:

 

  (5) C-D 边不会和已选 A-CB-DD-T 边构成环路,可以组成最小生成树:

 

  (6) C-B 边会和已选 C-DB-D 边构成环路,因此不能组成最小生成树:

 

  (7) B-TB-D-TA-BA-C-D-B构成环路,都不能组成最小生成树。而 S-A 不会和已选边构成环路,可以组成最小生成树。

 

  (8) 如上图所示,对于一个包含 6 个顶点的连通网,我们已经选择了 5 条边,这些边组成的生成树就是最小生成树。

 

  上面的过程中,我们运用了形象的判断环方法,但程序是很难实现的,因此需要做一些改动。对于树,都有一个基本特性:树在生长过程中,除了第一条边,其他边必然开始于已触达顶点,结束于未触达顶点。那么符合下面的条件就可以保证环不出现:

  (1)只有1棵树在生长;

  (2)除了首边,开始顶点已触达,结束顶点未触达。
  算法逻辑:

  (1)把第0边的开始顶点加入父顶点集,2个顶点都放到已触达顶点集中,第1边后放到当前扫描边集,声明一个空集作为下次扫描的边收集器。

  (2)扫描当前边集:

  (A)如果边的结束顶点已在已触达顶点集中,抛弃。然后,

  (B)如果开始顶点在已触达顶点集中,把这边加到树中:结束顶点加入已触达顶点集,开始顶点加入父顶点集。然后,

  (B1)如果已触达顶点达到总顶点数,完成,返回;

  (B2)否则把未扫描的边放入下次扫描的边收集器中,终止当次循环。

  (C)如果2点都不在已触达顶点集中,该边加入下次扫描的边收集器,继续当次循环。

  邻接矩阵程序代码(文本代码附录1,基础类在上一课):

 

 

 

 

 

 

  运行结果:

 

  邻接表程序代码(文本代码附录2):

 

 

 

 

 

 

  运行结果:

 

  结果正是下面的树:

 

 

  练习题1: 分别用邻接矩阵Kruskal和邻接表Kruskal算法计算下面图的最小生成树。基础类代码在上一课附录中,Kruskal算法函数代码在本棵附录中。

 

 

  学思营编程课堂基于蓝桥STEM86平台https://www.stem86.com,开展学编程三部曲:

Scratch(三年级以前)>>>Python(四年级以后)>>>C++(七年级以后)教育实验活动,任何人可以免费参加,打开https://xuesiying.stem86.com网页注册进入课堂,也可关注本公众号留言。

更多课程请打开学思营同步网站:

http://www.5xstar.com/doc.html

 

参考资料:

1、Kruskal算法(克鲁斯卡尔算法)详解http://c.biancheng.net/algorithm/kruskal.html

2、《Python算法图解》何韬编著 清华大学出版社

 

附录1:

#邻接矩阵Kruskal最小生成树
from 最小生成树 import MinGraphTree
from 图结构_邻接矩阵 import GraphMatrix
#Kruskal算法
def matrixKruskal(gMat):
    '''@gMat邻接矩阵图'''
    lines = __getLines(gMat)  #获取连线顺序表
    result = None
    value = None
    starts = []  #已经完成的开始边
    for i in range(len(lines)):  #0开始扫描起点,最小边数是大于0的偶数
        if value == None:  #获取最小值
            value = lines[i][2]
            starts.append(lines[i])
        elif lines[i][2] > value:  #如果最小边已完成
            break
        else:
            if __checkStart(starts, lines[i]):  #如果该边已经作为开始了,跳过
                continue
            else:
                starts.append(lines[i])
                lines[i], lines[0] = lines[0], lines[i]  #交换最小边位置到第0
        reachedVertexList, parents, sumValue = __matrixKruskal(gMat, lines)  #尝试一次
        #print(i,reachedVertexList, parents, sumValue)  #测试
        if reachedVertexList != None:  #找到一个解决方案
            if result == None or sumValue < result[2]:
                result = (reachedVertexList, parents, sumValue)
    if result == None:
        return None
    else:
        return MinGraphTree(result[0],result[1])
def __matrixKruskal(gMat, lines): #尝试lines[0]开始的最小树
    line = lines[0]  #取出0
    reachedVertexList = [line[0],line[1]] #已加的索引
    parents = [None, line[0]]  #父顶点索引
    values = [0, line[2]]  #权值
    lines1 = lines[1:]  #当次扫描
    lines2 = [] # 下次扫描
    while len(lines1)>0:
        for lineIndex in range(len(lines1)):
            line = lines1[lineIndex]  #取出1
            (start, end, value) = line  #解包一边
            if end in reachedVertexList:  #如果结束点已经在触达集,忽略
                continue
            elif start in reachedVertexList: #只开始端在已触达顶点
                reachedVertexList.append(end)  #把结束顶点加入触达集
                parents.append(start)  #把开始顶点加入父顶点集
                values.append(value)  #把权值加入权值集
                if len(reachedVertexList) == gMat.length:  #最小树生成成功
                    for i in range(gMat.length):  #转换触达集
                        reachedVertexList[i] = (gMat.dict[reachedVertexList[i]],values[i])
                    for i in range(1,gMat.length):  #转换parents, 第0个不用转换
                        parents[i] = gMat.dict[parents[i]]
                    return reachedVertexList, parents, sum(values)  #返回方案
                else: #还没完成
                    if lineIndex<len(lines1)-1:  #没到最后一个元素,把剩下的元素
                        lines2.extend(lines1[lineIndex+1:])  #复制入下次扫描集中
                    break  #中终止当次循环
            else:  #两点都不在触达集的元素放入下次扫描集中
                lines2.append(line)                     
        lines1 = lines2  #准备下次扫描
        lines2 = []
    return None,None,None     
def __getLines(gMat):  #获取连线顺序表
    lines = []  #连线收集表
    for i in range(gMat.length):  #扫描矩阵,取出边
        for j in range(gMat.length):
            v = gMat.matrix[i][j]
            if v > 0:
                lines.append((i, j, v))  #加入一边
    lines.sort(key=lambda line : line[2])
    #key = lambda line : line[2] 相当于
    #def fun(line):
    #    return line[2]
    #key = fun
    return lines   
def __checkStart(starts, line):  #检查是否已经在开始集中
    for start in starts:
        if start[0] == line[1] and start[1] == line[0]:  #同一边
            return True
    return False
#测试   激发《五行星学编程》使用idle运行 input()
if __name__ == "__main__":
    lst = ['A','B','C','D','S','T']
    lstA = [['B',6],['C',3],['S',7]]  #A连接BCS
    lstB = [['C',4],['D',2],['T',5]]  #B连接CDT
    lstC = [['S',8],['D',3]]  #C连接SD
    lstD = [['T',2]]  #D连接T
    gMat = GraphMatrix()  #Kruskal算法只适用于无向图
    for key in lst:
        gMat.addVertex(key)
    gMat.addLine(lst[0],lstA)
    gMat.addLine(lst[1],lstB)
    gMat.addLine(lst[2],lstC)
    gMat.addLine(lst[3],lstD)
    gMat.display()
    print('------------------------')
    mgTree = matrixKruskal(gMat)  #转为最小生成树
    if mgTree != None:
        mgTree.display()

 

附录2:

#邻接表Kruskal最小生成树
from 最小生成树 import MinGraphTree
from 图结构_邻接表 import GraphList
#Kruskal算法
def listKruskal(gList):
    '''@gList邻接表'''
    lines = __getLines(gList)  #获取连线顺序表
    result = None
    value = None
    starts = []  #已经完成的开始边
    for i in range(len(lines)):  #0开始扫描起点,最小边数是大于0的偶数
        if value == None:  #获取最小值
            value = lines[i][2]
            starts.append(lines[i])
        elif lines[i][2] > value:  #如果最小边已完成
            break
        else:
            if __checkStart(starts, lines[i]):  #如果该边已经作为开始了,跳过
                continue
            else:
                starts.append(lines[i])
                lines[i], lines[0] = lines[0], lines[i]  #交换最小边位置到第0
        reachedVertexList, parents, sumValue = __listKruskal(gList, lines)  #尝试一次
        #print(i,reachedVertexList, parents, sumValue)  #测试
        if reachedVertexList != None:  #找到一个解决方案
            if result == None or sumValue < result[2]:
                result = (reachedVertexList, parents, sumValue)
    if result == None:
        return None
    else:
        return MinGraphTree(result[0],result[1])
def __listKruskal(gList, lines): #尝试lines[0]开始的最小树
    line = lines[0]  #取出0
    reachedVertexList = [line[0],line[1]] #已加的键
    parents = [None, line[0]]  #父顶点键
    values = [0, line[2]]  #权值
    lines1 = lines[1:]  #当次扫描
    lines2 = [] # 下次扫描
    while len(lines1)>0:
        for lineIndex in range(len(lines1)):
            line = lines1[lineIndex]  #取出1
            (start, end, value) = line  #解包一边
            if end in reachedVertexList:  #如果结束点已经在触达集,忽略
                continue
            elif start in reachedVertexList: #只开始端在已触达顶点
                reachedVertexList.append(end)  #把结束顶点加入触达集
                parents.append(start)  #把开始顶点加入父顶点集
                values.append(value)  #把权值加入权值集
                if len(reachedVertexList) == len(gList.dict):  #最小树生成成功
                    for i in range(len(gList.dict)):  #转换触达集
                        reachedVertexList[i] = (reachedVertexList[i],values[i])
                    return reachedVertexList, parents, sum(values)  #返回方案
                else: #还没完成
                    if lineIndex<len(lines1)-1:  #没到最后一个元素,把剩下的元素
                        lines2.extend(lines1[lineIndex+1:])  #复制入下次扫描集中
                    break  #中终止当次循环
            else:  #两点都不在触达集的元素放入下次扫描集中
                lines2.append(line)                     
        lines1 = lines2  #准备下次扫描
        lines2 = []
    return None,None,None     
def __getLines(gList):  #获取连线顺序表
    lines = []  #连线收集表
    for key in gList.dict:  #扫描邻接表,取出边
        row = gList.dict[key]  #邻接表中一行
        for (otherKey, value) in row:
            lines.append((key, otherKey, value))  #加入一边
    lines.sort(key=lambda line : line[2])
    #key = lambda line : line[2] 相当于
    #def fun(line):
    #    return line[2]
    #key = fun
    return lines    
def __checkStart(starts, line):  #检查是否已经在开始集中
    for start in starts:
        if start[0] == line[1] and start[1] == line[0]:  #同一边
            return True
    return False
#测试   激发《五行星学编程》使用idle运行 input()
if __name__ == "__main__":
    lst = ['A','B','C','D','S','T']
    lstA = [['B',6],['C',3],['S',7]]  #A连接BCS
    lstB = [['C',4],['D',2],['T',5]]  #B连接CDT
    lstC = [['S',8],['D',3]]  #C连接SD
    lstD = [['T',2]]  #D连接T
    gList = GraphList() #Kruskal算法只适用于无向图
    for key in lst:
        gList.addVertex(key)
    gList.addLine(lst[0],lstA)
    gList.addLine(lst[1],lstB)
    gList.addLine(lst[2],lstC)
    gList.addLine(lst[3],lstD)
    gList.display()
    print('------------------------')
    mgTree = listKruskal(gList)  #转为最小生成树  
    if mgTree != None:
        mgTree.display()