图论:最小生成树

最小生成树的三种经典解法

Posted by Charles Xu on August 12, 2018

Basic Knowledge

树(Tree): 如果无向连通图中不存在回路,则这种图称为树。

森林(Forest): 如果无向图中包含了几棵树,那么该无向图可以称为森林,森林为非连通图。

生成树(Spanning Tree):如果无向连通图 \(G\) 的子图是一棵包含了 \(G\) 的所有顶点的树,那么该子图叫做 \(G\) 的生成树,生成树是连通图的极小连通子图,加一条边就会构成回路,减一条边,就会变成非连通图;

最小生成树(Minimum Spanning Tree):

minimum spanning tree (MST) or minimum weight spanning tree is a subset of the edges of a connected, edge-weighted (un)directed graph that connects all the vertices together, without any cycles and with the minimum possible total edge weight. 

并查集(Union-find Set): 是一种树型的数据结构,用于处理一些不交集(Disjoint Sets)的合并及查询问题。有一个联合-查找算法定义了两个用于此数据结构的操作:

  • Find:确定元素属于哪一个子集。它可以被用来确定两个元素是否属于同一子集。
  • Union:将两个子集合并成同一个集合。
class UFset:
    """
    Union-find class
    """

    def __init__(self, n):
        self.n = n
        self.parent = [-1 for i in range(0, n + 1)]

    def find(self, x):
        p = x
        while self.parent[p] >= 0:
            p = self.parent[p]

        while x != p:
            t = self.parent[x]
            self.parent[x] = p
            x = t
        return p

    def union(self, n, m):
        r1 = self.find(n)
        r2 = self.find(m)
        t = self.parent[r1] + self.parent[r2]
        if self.parent[r1] > self.parent[r2]:
            self.parent[r1] = r2
            self.parent[r2] = t
        else:
            self.parent[r2] = r1
            self.parent[r1] = t

    def print(self):
        for i in range(1, self.n + 1):
            print(self.parent[i], end=' ')

等价类(Equivalent Class): 在数学中,假设在集合 \(X\) 上的一个等价关系(~),则 \(X\) 中的某个元素 a 的等价类就是在 \(X\) 中等价于 a 的所有元素形成的子集。


Minimum Undirected Spanning Tree

问题定义:

图 $G = (V, E, w)$ 是一个带权连通无向图,$w : E \rightarrow \mathbb{R}$ 是定义在边上的权重。我们希望找到图 \(G\)

的一个一个无环子集 \(T \in E\) ,既能够连接所有的顶点,又具有最小的权重,即 \(w(T) = \sum_{(u,v) \in T}w(u, v)\) 的值最小。由于 \(T\) 是无环的,并且连通所有的顶点,所以 \(T\) 必然是一棵树。我们把这样的树称之为图 \(G\) 的最小生成树。

构造生成树的原则:

  1. 必须使用图中的边来构造;
  2. 必须使用且仅能使用 n-1 条边来连接图中 n 个顶点;
  3. 不能使用产生回路的边;

生成树的构建:

GENERIC-MST(G, w)

1	A = ∅

2	while A does not form a spanning tree

3		find an edge(u, v) that is safe for A

4		A = A ∪ {(u, v)}

5	return A

在上述过程中,构造 $MST$ 的过程,就是不断寻找 \(A\) 的安全边的过程。而 \(A\) 被包含在图的某一棵 \(MST\) 中。那么究竟什么样的边,对于 \(A\) 来说是一条 safe edge 呢?通俗理解:就是这条边加入 \(A\) 后还要能保证其是图的某一棵 \(MST\) 一个子集。下面,给出一些定义:

  • \((S, V-S)\) ,是图 \(G\) 顶点集合 \(V\) 的一个划分;
  • \((u, v)\) 横跨切割 \((S, V-S)\),当且仅当 \((u, v) \in E\),并且两个端点分别属于两个切割的两个集合;
  • 切割尊重集合 \(A\) ,说明 \(A\) 中不存在横跨切割的边;
  • 轻量级边,在横跨切割的所有边中权重最小的那一条边,不一定唯一;

定理: \(A\) 是边的集合,且被包含在 \(G\) 的某一棵 \(MST\) 中;\((S, V-S)\) 是图 \(G\) 顶点集合 \(V\) 的一个切割,并且其尊重 \(A\);边 \((u, v)\) 是横跨切割 \((S, V-S)\) 的一条轻量级边;那么, \((u, v)\) 对于 \(A\) 是安全的。

下面讨论的一些经典算法实际上都是寻找安全边的不同策略,这些算法的核心思想都是贪心策略,\(MST\) 问题也是利用贪心策略能够获得最优解的特例之一。

Classic Algorithms

Kruskal

思路: 寻找安全边的方法是,在所有连接森林中两棵不同树的边里面,寻找最小的边 \((u, v)\)。

MST-KRUSKAL(G, w)

1	A = ∅

2	for each vertex v ∈ G.V

3		MAKE-SET(v)

4	sort the edges of G.E into nodecreasing order by weight w

5 	for each edge (u, v) ∈ G.E, take in nodecreasing order by weight

6		if FIND-SET(u) ≠ FIND-SET(v)

7			A = A ∪ {(u, v)}

8			UNION(u, v)

9	return A 

复杂度: 时间复杂度为 \(O( \vert E \vert lg \vert E \vert )\),主要取决于边数,适合稀疏图。

图例:

代码:

from collections import namedtuple

def Kruskal():
    mst_weight = 0
    selected_edge_num = 0
    V = set()
    E = list()
    Edge = namedtuple('Edge', ['u', 'v', 'w'])
    with open('2.15') as fp:
        for line in fp.readlines():
            u, v, w = [int(x) for x in line.split()]
            E.append(Edge(u, v, w))
            V.add(u)
            V.add(v)
    # Sort edges by weight.
    E = sorted(E, key=lambda x: x.w)
    # Use the union-find set to check weather the new edge will lead to cycle.
    c = UFset(len(V))
    for (u, v, w) in E:
        if c.find(u) != c.find(v):
            print(u, v, w)
            mst_weight += w
            c.union(u, v)
            selected_edge_num += 1
            if selected_edge_num == len(V) - 1:
                break
    print("The weight of MST is:", mst_weight)

Prim

思路: Prim 算法具有一个性质是集合 \(A\) 中的边总是构成一棵树。这棵树从某一顶点 r 开始,每一次扩展所加入的边必须是使得树的总权重增加最少的边,最终形成图的一棵最小生成树。

MST-PRIM(G, w)

1	for each vertex u ∈ G.V

2		u:key = ∞	# 连接该顶点和树中顶点的最小权重,如果不存在为无穷大

3		u:π = NIL	# 顶点在树中的父节点

4	r.key = 0

5 	Q = G.V

6	while Q ≠ ∅

7		u = EXTRACT-MIN(Q)	# 轻量级边在Q中的端点

8		for each v ∈ G.Adj[u]	# 更新与 u 相邻的Q中顶点的信息

9			if v ∈ Q and w(u, v) < v.key 

10				v.key = w(u, v)

11				v.π = u

复杂度: 时间复杂度为 \(O(\vert V \vert ^2)\),与图中的边数无关,适合稠密图。

图例:

代码:

def Prim():
    mst_weight = 0
    V = set()
    E = list()
    Edge = namedtuple('Edge', ['u', 'v', 'w'])
    with open('2.15') as fp:
        for line in fp.readlines():
            u, v, w = [int(x) for x in line.split()]
            E.append(Edge(u, v, w))
            V.add(u)
            V.add(v)

    is_used = {x: 0 for x in E}
    c = UFset(len(V))
    Q = set()
    root = V.pop()
    V.add(root)
    Q.add(root)
    while Q != V:
        selected_edge = None
        selected_edge_weight = 65536
        for edge in E:
            if is_used[edge] == 0:
                u, v, w = edge
                if (c.find(u) == root and c.find(v) != root) or (c.find(v) == root and c.find(u) != root):
                    if w < selected_edge_weight:
                        selected_edge = edge
                        selected_edge_weight = edge.w

        if selected_edge:
            u, v, w = selected_edge
            print(u, v, w)
            c.union(u, v)
            mst_weight += w
            is_used[selected_edge] = 1

        for v in V:
            if c.find(v) == root:
                Q.add(v)

    print("The weight of MST is:", mst_weight)
    

Boruvka

思路: 该算法是最古老的 \(MST\) 算法,其思想类似于 Kruskal 和 Prim。Boruvka 算法可以分为两步:1)对图中的各顶点,将与其关联的、具有最小权重的边选入 \(MST\) ,得到 \(MST\) 子树构成的森林;2)在途中陆续选择可以连接两棵不同子树,且具有最小权值的边将子树进行合并,最终构成单一连通分量 \(MST\)。

MST-BORUVKA(G, w)

1 Initial a forest T to be a set of one-vertex trees, one for each vertex of the graph

2 while T has more than one component:

3 for each component C of T:

4 begin with an empty set of edges S

5 for each vertex v in C:

6 find the cheapest edge from v to a vertex outside of C and add it to S

7 add the cheapest edge in S to T

8 combine trees connected by edges to form bigger components

9 Output: T is the minimum spanning tree of G

复杂度: 每一次循环都会将不连通分量的个数减半,一开始不连通分量的个数为 \(\vert V \vert\),所以循环将要进行 \(log(\vert V \vert)\) 次。在每次循环的内部,需要检查每一条边,所以总的时间复杂度是 \(O(\vert E \vert lg \vert V \vert)\)。

图例:

代码:

from collections import namedtuple
def Boruvka():
    mst_weight = 0
    V = set()
    E = list()
    Edge = namedtuple('Edge', ['u', 'v', 'w'])
    with open('2.15') as fp:
        for line in fp.readlines():
            u, v, w = [int(x) for x in line.split()]
            E.append(Edge(u, v, w))
            V.add(u)
            V.add(v)
    # Use the union-find set to check weather the new edge will lead to cycle.
    c = UFset(len(V))

    Q = set()
    for v in V:
        Q.add(c.find(v))
    while len(Q) > 1:
        # In each loop, find the shortest edge of each set which links this set to another set.
        selected_edges = set()
        for n in Q:
            shortest_linked_edge = None
            shortest_linked_edge_length = 65536
            for edge in E:
                u, v, w = edge
                if (c.find(n) == c.find(u) and c.find(n) != c.find(v)) or (
                        c.find(n) == c.find(v) and c.find(n) != c.find(u)):
                    if w < shortest_linked_edge_length:
                        shortest_linked_edge_length = w
                        shortest_linked_edge = edge
            # Avoid selecting the same edge twice.
            if shortest_linked_edge and shortest_linked_edge not in selected_edges:
                selected_edges.add(shortest_linked_edge)
        # According to the selected edges, combine sets.
        for (u, v, w) in selected_edges:
            print(u, v, w)
            c.union(u, v)
            mst_weight += w
        Q.clear()
        for v in V:
            Q.add(c.find(v))
    print("The weight of MST is:", mst_weight)
    

References

  1. 维基百科:Minimum spanning tree

  2. 维基百科:Borůvka’s algorithm

  3. 《算法导论》(第三版)第 23 章 最小生成树;

  4. 《图论引论》(第二版)第 2.3 节 最小生成树;

  5. 《图论算法理论实现与应用》第 3.2 节 生成树及最小生成树;

  6. 网络文档:Boruvka’s Algorithm