前言

最近在研究使用序列模型来实现图像任务的深度学习,发现将图像数据转换成序列数据总会存在空间位置信息的损失,而图结构能够提供一个很强大的表征方式,因此希望研究一下如何将其进行进一步的应用。

为了深刻的理解图结构,这里从一个最简单的用最小生成树表示无向图的例子进行切入。

什么是无向图

无向图(Undirected Graph)是一种图结构,在这种图中,边(Edge)没有方向性。换句话说,如果存在一条边连接两个顶点(Vertex),则可以从任意一个顶点到达另一个顶点。无向图通常表示为 ( G = (V, E) ),其中:

在无向图中,边 ( (u, v) ) 表示顶点 ( u ) 和顶点 ( v ) 之间存在一条边,且 ( (u, v) ) 和 ( (v, u) ) 是等价的。

示例

考虑一个无向图 ( G ) ,其顶点集合 ( V = {1, 2, 3, 4} ) ,边集合 ( E = {(1, 2), (2, 3), (3, 4), (4, 1)} )。这个图可以表示为:

    1 -- 2
    |    |
    4 -- 3

无向图的特性

  1. 对称性:如果边 ( (u, v) ) 存在,则边 ( (v, u) ) 也存在。
  2. 度(Degree):一个顶点的度是连接到该顶点的边的数量。
  3. 连通性:如果从图中的任意一个顶点都能到达其他任意一个顶点,则这个图是连通的。

无向图在很多领域都有应用,例如网络拓扑、社交网络分析、生物网络等。

什么是最小生成树

最小生成树(Minimum Spanning Tree,简称 MST)是无向加权图中的一个子图,它包含图中的所有顶点,并且使得所有边的权重之和最小。换句话说,最小生成树是连接图中所有顶点的权重最小的无环子图。

特性

  1. 唯一性:一个无向图可能有多个最小生成树,但对于给定的边权重集合,最小生成树的总权重是唯一的。
  2. 连通性:最小生成树必须包含图中的所有顶点,且是连通的。
  3. 无环性:最小生成树是无环的,否则可以通过去掉环中的某条边来减少总权重。

应用

最小生成树在许多实际问题中有重要应用,例如:

常见算法

  1. Kruskal算法:通过排序所有边并逐步选择不形成环的最小边来构建最小生成树。
  2. Prim算法:从一个顶点开始,逐步选择最小边扩展生成树,直到包括所有顶点。

算法思路

对于一个具有n个顶点的无向图,最多有$\frac{n(n-1)}{2}条边$ 举一个具有6个顶点的无向图为例

 (0)-1-(1)
  | \   |  \
  5  2  3   3
  |   \ |    \
 (2)   (3)-1-(5)
   \    |    /
    3   2   4
     \  |  /
       (4)

这里设置了9条边,将它表示为列表,并且按照边的权重从小到大排序

[
# (顶点1,顶点2,边权重)
(0, 1, 1),
(3, 5, 1),
(0, 3, 2),
(3, 4, 2),
(1, 5, 3),
(1, 3, 3),
(2, 4, 3),
(4, 5, 4),
(0, 2, 5),
]

利用迭代来找出最小的连通图。

第一轮迭代

选择最小边

对于连通分量0,最小的边是(0, 1, 1)
对于连通分量1,最小的边是(0, 1, 1)
对于连通分量2,最小的边是(2, 4, 3)
对于连通分量3,最小的边是(3, 5, 1)
对于连通分量4,最小的边是(3, 4, 2)
对于连通分量5,最小的边是(3, 5, 1)

合并连通分量

选择(0, 1, 1)合并连通分量0和1。

 (0)-1-(1)
            
            
  
 (2)   (3)  (5)
 
 
 
       (4)

选择(3, 5, 1)合并连通分量3和5。

 (0)-1-(1)
            
            
  
 (2)   (3)-1-(5)
 
 
 
       (4)

选择(3, 4, 2)合并连通分量3和4。

 (0)-1-(1)
            
            
  
 (2)   (3)-1-(5)
        |
        2
        |
       (4)

选择(2, 4, 3)合并连通分量2和4。

 (0)-1-(1)
            
            
  
 (2)   (3)-1-(5)
   \    |
    3   2
     \  |
       (4)

更新并查集

第二轮迭代

选择最小边

对于连通分量0-1,最小的边是(0, 3, 2)
对于连通分量2-3-4-5,最小的边是(0, 3, 2)

选择(0, 3, 2)合并连通分量0和3。

 (0)-1-(1)
    \
     2
      \ 
 (2)   (3)-1-(5)
   \    |
    3   2
     \  |
       (4)

更新并查集,发现所有的顶点都已连通,算法结束

算法实现

class UnionFind:
    def __init__(self, n):
        self.parent = list(range(n))
        self.rank = [0] * n

    def find(self, u):
        if self.parent[u] != u:
            self.parent[u] = self.find(self.parent[u])
        return self.parent[u]

    def union(self, u, v):
        root_u = self.find(u)
        root_v = self.find(v)

        if root_u != root_v:
            if self.rank[root_u] > self.rank[root_v]:
                self.parent[root_v] = root_u
            elif self.rank[root_u] < self.rank[root_v]:
                self.parent[root_u] = root_v
            else:
                self.parent[root_v] = root_u
                self.rank[root_u] += 1

def contractive_boruvka(edges, n):
    uf = UnionFind(n)
    mst_edges = []
    mst_weight = 0
    iteration = 0

    while len(mst_edges) < n - 1:
        iteration += 1
        print(f"Iteration {iteration}:")

        # 初始化每个组件的最小边
        min_edge = [-1] * n

        for index, (u, v, weight) in enumerate(edges):
            root_u = uf.find(u)
            root_v = uf.find(v)
            if root_u != root_v:
                if min_edge[root_u] == -1 or edges[min_edge[root_u]][2] > weight:
                    min_edge[root_u] = index
                if min_edge[root_v] == -1 or edges[min_edge[root_v]][2] > weight:
                    min_edge[root_v] = index

        # 收缩每个组件的最小边
        for edge_index in min_edge:
            if edge_index != -1:
                u, v, weight = edges[edge_index]
                root_u = uf.find(u)
                root_v = uf.find(v)
                if root_u != root_v:
                    uf.union(u, v)
                    mst_edges.append((u, v, weight))
                    mst_weight += weight
                    print(f"  Connecting {u} - {v} with weight {weight}")

        print(f"  MST so far: {mst_edges}")
        print()

    return mst_edges, mst_weight

# 示例使用
edges = [
	# (顶点1,顶点2,边权重)
	(0, 1, 1),
	(3, 5, 1),
	(0, 3, 2),
	(3, 4, 2),
	(1, 5, 3),
	(1, 3, 3),
	(2, 4, 3),
	(4, 5, 4),
	(0, 2, 5),
]
n = 6

mst_edges, mst_weight = contractive_boruvka(edges, n)
print("Final MST edges:", mst_edges)
print("Total weight of MST:", mst_weight)