#
# 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
#
# 返回最小的花费代价使得这n户人家连接起来
# @param n int整型 n户人家的村庄
# @param m int整型 m条路
# @param cost int整型二维数组 一维3个参数,表示连接1个村庄到另外1个村庄的花费的代价
# @return int整型
#
class UnionFind:
    def __init__(self, n):
        self.parent=list(range(n+1))
        self.rank=[0]*(n+1)
    # def find(self,x): #找到x所在的顶点集的根节点
    #     while x!=self.parent[x]:#如果 x 不是根节点
    #         x=self.parent[x]
    #     return x
    def find(self,x):#查找的时候进行路径压缩
        while x!=self.parent[x]:#如果 x 不是根节点
            self.parent[x]=self.parent[self.parent[x]] #压缩,把x挂在它的‘爷爷’结点上
            x=self.parent[x]
        return x

    def union(self,x,y):
        rx,ry=self.find(x),self.find(y)
        if rx!=ry:#按秩合并,低秩根挂在高秩根下
            if self.rank[rx]<self.parent[ry]:
                self.parent[rx]=ry
            elif self.rank[rx]>self.parent[ry]:
                self.parent[ry]=rx
            else: #==
                self.parent[rx]=ry
                self.parent[ry]+=1 #秩要加1
            return True
        return False #根据Cycle Property, 这条边不属于任何最小生成树
 
class Solution:
    def miniSpanningTree(self , n: int, m: int, cost: List[List[int]]) -> int:
        # write code here
        #Cut Property: 任意地把顶点集分为两部分(称为一个割),跨割最小边必定出现在某颗最小生成树中
        #Cycle Property: 在任意带权图的某个环中,权重大于环中其他任何边的那条边,不可能属于任何最小生成树
        cost.sort(key=lambda edge:edge[2])#根据修路成本排序
        uf=UnionFind(n)
        edges=0
        money=0
        for u,v,w in cost: #Kruskal算法,贪心加入最短边
            if uf.union(u,v): #合并成功
                edges+=1
                money+=w
                if edges==n-1:
                    break
        return money

根据Cut Property和 Cycle Property,我们知道可以先对边按照权重进行排序,然后根据贪心原理、每次加入最短边来连通两个子集,并且舍弃会造成环的边(经过排序后,必然是环中权重最大的边,所以要舍弃)。其实也就是 Kruskal 算法。

关于如何判断加入边的时候、两个顶点是否在同一个子集从而是否会造成环,就需要用到并查集,然后再查找的过程中可以进行路径压缩、优化后续查找的时间复杂度。