图-最小生成树, 2022-10-30

(2022.10.30 Sun)
生成树(Minimal Spanning Tree,MST)的概念针对连通图而提出。

概念和定理

  • 连通图(connected graph):无向图(undirected graph)中,如果任意两点有路径连接,则称其为连通图(connected graph)
  • 强连通图:在有向图(directed graph)中,任意两点v_i,v_j都有路径相连接,则称其为强连通图
  • 生成树:连通图的生成树指一个连通子图,含有图中的全部n个顶点,只有足以构成一棵树的n-1条边。在该树中再添加一条边,则必定成环
  • 最小生成树:设连通图的每个边有权重,在一个连通图的所有生成树中,权重之和最小的那课树定义为最小生成树

在无向图中查找MST,可通过greedy method实现,本文后面将要介绍的两种方法都是基于该方案。在介绍算法之前,先了解关于MST的一个重要事实。

Let G be a weighted connected graph, and let V1 and V2 be a
partition of the vertices of G into two disjoint nonempty sets. Furthermore, let e be an edge in G with minimum weight from among those with one endpoint in V1 and the other in V2. There is a minimum spanning tree T that has e as one of its edges.

加权连通图G,其中V1和V2分别是G的两个点集合,且V1和V2不连接。设边e是V1和V2之间的任意点连接的边中权重最小的边,则e是该图MST的一边。

证明:
令T是图G的最小生成树。如果T不包含边e,则在T中加入e会形成一个环(cycle)。因此在T中有一条边满足f  \neq e,且f的两个顶点分别位于V1和V2。考虑到权重值e最小,则有w(e) \leq w(f)。如果从T ∪\{e\}中移除f,就得到了权重比之前小的一棵MST。考虑到T是MST,则替换f为e的新树也必然为MST。

Let T be a minimum spanning tree of G. If T does not contain
edge e, the addition of e to T must create a cycle. Therefore, there is some edge f  \neq e of this cycle that has one endpoint in V1 and the other in V2. Moreover, by the choice of e, w(e) \leq w( f ). If we remove f from T ∪\{e\}, we obtain a spanning tree whose total weight is no more than before. Since T was a minimum spanning tree, this new tree must also be a minimum spanning tree.

Kruskal算法

Kruskal算法在最初将每个独立顶点当做一棵树,并重复将不在一棵树上的顶点融合成一个簇,最终该簇成为MST。

该算法对边做排序,根据权重升序(ascending)排序,从排序中依次考虑各边。如果一条选中的边连接了两颗树,则将该边加入到MST中,且这两颗树融合为一个簇。两个不同的簇可融合为一个簇。如果按排序选中的边e所连接的两个树/簇已经在同一个簇中,则抛弃该边e。当边的个数足够多,达到n-1,则过程终止,得到的簇即MST。

Complexity analysis

该算法的计算分为两部分:edge sorting和test whether 2 clusters are distinct。

边排序部分采用快速等方法,复杂度为O(m\log m),其中m表示边的个数。对于一般图来说,m为O(n^2)(?),因此O(\log m)等效于O(\log n),所以排序部分的复杂度为O(m\log n)

第二部分,placeholder。

总复杂度是O(m \log n)

Codes

import logging

class Edge:
    """
    edge structure, two ends, i.e., u and v, and weight incoporated
    """
    def __init__(self, u, v, weight):
        self.u = u
        self.v = v
        self.weight = weight

class DisjointSet:
    def __init__(self, n):
        """
        n: number of vertices
        """
        self.parent = [None] * n # parenet of node i
        self.size = [1] * n # for merge
        for i in range(n):
            self.parent[i] = i # set each vertex as its only parent

    def merge_set(self, a, b):
        """
        merge two vertices, a and b, and the trees they belong to

        Arguments:
          a - index of a
          b - index of b
        Returns:
          if a and b share no the same root, then merge them
        """
        a = self.find_set(a)
        b = self.find_set(b)
        logging.info(f"""merging: vertex {a+1} and {b+1}""")

        if self.size[a] < self.size[b]:
            logging.info(f"""merging: size.{a+1} < size.{b+1}""")
            self.parent[a] = b # merge a to b
            self.size[b] += self.size[a] # add size of old set(a) to set(b)
        else:
            logging.info(f"""merging: size.{a+1} >= size.{b+1}""")
            self.parent[b] = a # merge b to a
            self.size[a] += self.size[b] # add size of old set(b) to set(a)

    def find_set(self, a):
        """
        find out root of node a, or the set which names after root
        """
        if self.parent[a] != a: 
            # find it out in a recurrsive way 
            self.parent[a] = self.find_set(self.parent[a])
        # return root
        return self.parent[a]


def k_algo(n, edges, ds):
    """
    Kruskal algorithm: 
      Rank all edges in an ascendent order of weights, find out top n-1 edges with no loop.
      Select a new edge from edge ranking, if the two ends of that edge are not in the disjoint set,
      then add the edge to MST. Otherwise, adding the edge leads to a loop. The final results contain 
      n-1 edges.

    Arguments:
      n - int, #vertices
      edges - list, graph edges
      ds - DisjointSet
    Returns:
      sum(weights) - MST weight summary
    """

    edges.sort(key=lambda x: x.weight) # sort edges by weights
    MST = [] 
    for edge in edges:
        set_u = ds.find_set(edge.u) # root of u 
        set_v = ds.find_set(edge.v) 
        if set_u != set_v: 
            logging.info(f"""Vertices {u+1} and {v+1} have different roots.""")
            # u and v have no same root, then merge
            ds.merge_set(set_u, set_v)
            MST.append(edge)
            if len(MST) == n-1: 
                # reach to the minimal number of edge, simply stop
                logging.info(f"""MST reaches to the end.""")
                break
    edges_selected = []
    for e in MST:
        edges_selected.append((e.u+1, e.v+1, e.weight))
    logging.info(f"""MST edges: {edges_selected}""")
    return sum([edge.weight for edge in MST])

if __name__ == "__main__":
    format = "%(asctime)s: %(message)s"
    logging.basicConfig(format=format, level=logging.INFO)
    n = 6 # num of vertices in the weighted undirected graph
    # assume the vertex index are continuous integers starting from 1 
    e = [(1, 2, 10), (3, 5, 19), (4, 6, 7), 
         (1, 5, 3),  (2, 6, 31), (3, 6, 20)]
    m = len(e) # num of edges
    ds = DisjointSet(m)
    edges = [None] * m 

    # allocate edge info 
    for i in range(m):
        u, v, weight = e[i][0], e[i][1], e[i][2]
        u -= 1 # transform index, [1, n] -> [0, n-1]
        v -= 1 
        edges[i] = Edge(u, v, weight)

    print("MST weights sum:", k_algo(n, edges, ds))

Reference

1 Data Structure and Algorithm in Python, Goodrich and etc.

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,539评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,911评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,337评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,723评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,795评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,762评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,742评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,508评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,954评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,247评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,404评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,104评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,736评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,352评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,557评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,371评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,292评论 2 352

推荐阅读更多精彩内容