最小生成树定义
设 G = ( V , E ) G = (V, E) G=(V,E) 是一个无向连通网,如果连通图的一个子图是一棵包含所有顶点的树(顶点数 = 边数 + 1),则该子图称为 G G G 的生成树(Spanning Tree)。
- 连接图中所有的 n n n 个点,并且只有 n − 1 n-1 n−1 条边的子图就是它的生成树;
- 生成树是连通图的包含图中的所有顶点的极小连通子图;
- 图的生成树不惟一。从不同的顶点出发进行遍历,可以得到不同的生成树;
- 只要能连通所有顶点而又不产生回路的任何子图都是它的生成树。
E E E 中每一条边 ( u , v ) (u, v) (u,v)上的权值 c ( u , v ) c(u, v) c(u,v),称为 ( u , v ) (u, v) (u,v) 的边长。图 G G G 的生成树上各边的权值(边长)之和称为该生成树的代价。
在图 G G G 所有生成树中,代价最小的生成树称为最小生成树(Minimum-Cost Spanning Tree, MST)。
- 明确一点,最小生成树可能不是唯一的,但是最小生成树的权是唯一的。
最小生成树可以应用到许多实际问题。例如,在 n n n 个教室之间建局域网络,至少要架设 n − 1 n-1 n−1 条通信线路,而每两个教室之间的距离可能不同,从而架设通信线路的造价就是是不一样的,那么如何设计才能使得总造价最小?
最小生成树的性质——贪心选择性证明
贪心选择性:假设 G = ( V , E ) G =(V, E) G=(V,E) 是一个连通网, U U U 是顶点集 V V V 的一个非空真子集。若 ( u , v ) (u,v) (u,v) 是一条具有最小权值(代价)的边,其中 u ∈ U , v ∈ V − U u∈U, v∈V-U u∈U,v∈V−U,则必存在一棵包含边 ( u , v ) (u,v) (u,v) 的最小生成树。
此性质保证了 P r i m Prim Prim 和 K r u s k a l Kruskal Kruskal 贪心算法的正确性。
MST贪心选择性的证明:(反证法)
【反证】设 G G G 的任何一棵最小生成树都不包含 ( u , v ) (u,v) (u,v),设 T T T 是连通网 G G G 上的一棵最小生成树。将边 ( u , v ) (u,v) (u,v) 加入到 T中,由生成树的定义,此时 T T T 中必包含一条 ( u , v ) (u,v) (u,v) 的回路:
- 由于 T T T 是生成树,则在 T T T 上必存在另一条边 ( u ’ , v ’ ) (u’,v’) (u’,v’),且 u u u 和 u ’ u’ u’、 v v v 和 v ’ v’ v’ 之间均有路径相通,即将边 ( u , v ) (u,v) (u,v) 加入到 T T T 中时,形成了环。
删去边 ( u ’ , v ’ ) (u’,v’) (u’,v’) 便可消去上述回路,得到生成树 T ′ T' T′。但因为 ( u , v ) (u, v) (u,v) 的代价不高于 ( u ’ , v ’ ) (u’,v’) (u’,v’),所以 T ′ T' T′ 的代价亦不高于 T T T,
- 如果 c ( u , v ) = c ( u ’ , v ’ ) c(u, v)=c(u’,v’) c(u,v)=c(u’,v’),此时得到了另一棵最小生成树 T ′ T' T′,与 “ G G G 的任何一棵最小生成树都不包含 ( u , v ) (u,v) (u,v)” 矛盾;
- 如果 c ( u , v ) < c ( u ’ , v ’ ) c(u, v)<c(u’,v’) c(u,v)<c(u’,v’),此时得到的生成树 T ′ T' T′ 的代价小于 T T T 的代价,与 “ T T T 是连通网 G G G 上的一棵最小生成树” 矛盾;
性质得证。
Prim 算法
Prim 算法流程
设无向连通图为 G = ( V , E ) G=(V, E) G=(V,E),令 G G G 的最小生成树为 T = ( U , T E ) T=(U, TE) T=(U,TE)。
算法过程:
- 首先从顶点集 V V V 中任取一顶点(如顶点 v 1 v_1 v1)放入集合 U U U(已访问点集) 中。这时 U = { v 1 } , T E = { } U=\{v_1\} ,TE=\{ \} U={v1},TE={};
- 然后找出权值最小的边 ( u , v ) (u,v) (u,v),且 u ∈ U , v ∈ ( V − U ) u∈U,v∈(V-U) u∈U,v∈(V−U),将边加入 T E TE TE,并将顶点 v v v 加入集合 U U U;
- 重复上述操作直到 U = V U=V U=V 为止。这时 T E TE TE 中有 n − 1 n-1 n−1 条边, T = ( U , T E ) T=(U,TE) T=(U,TE) 就是 G G G 的一棵最小生成树。
上面第二步中,如何找到连接 U U U 和 V − U V-U V−U 的最短边?
- 类似于 Dijkstra 算法;
- 利用 MST 性质,可以对于 V − U V-U V−U 中的每个顶点,保存从该顶点到 U U U 中的各顶点的最短边;
Prim 算法实现
需要用到的数据结构:
- 数组
L
O
W
C
O
S
T
[
n
]
LOWCOST[n]
LOWCOST[n]:用来保存集合
V
−
U
V-U
V−U 中各顶点与集合
U
U
U 中顶点最短边的权值;
- L O W C O S T [ v ] = 0 LOWCOST[v]=0 LOWCOST[v]=0 表示顶点 v v v 已加入最小生成树中,也就是顶点 v v v 已经被访问过;
- L O W C O S T [ v ] = i n f i n i t y LOWCOST[v]=infinity LOWCOST[v]=infinity 表示从顶点 v v v 无法到达当前已访问顶点集 U U U。
- 数组 C L O S S E T [ n ] CLOSSET[n] CLOSSET[n]:用来保存依附于该边的(集合 V − U V-U V−U 中各顶点与集合 U U U 中顶点的最短边)在集合 U U U 中的顶点。
如何用数组 L O W C O S T [ n ] LOWCOST[n] LOWCOST[n] 和 C L O S S E T [ n ] CLOSSET[n] CLOSSET[n] 表示候选最短边集? L O W C O S T [ i ] = w & C L O S S E T [ i ] = k LOWCOST[i]=w\ \&\ CLOSSET[i] = k LOWCOST[i]=w & CLOSSET[i]=k 表示顶点 v i , v k v_i,v_k vi,vk 之间的权值为 w w w,其中 v i ∈ V − U , v k ∈ U v_i \in V-U, v_k \in U vi∈V−U,vk∈U。
如何更新这两个数组?
- 当在 L O W C O S T [ n ] LOWCOST[n] LOWCOST[n] 数组中找到距离当前访问集 U U U 权值最小的边对应的顶点 v i v_i vi 之后,将 L O W C O S T [ i ] LOWCOST[i] LOWCOST[i] 置为 0,表示该点已经被访问;
- 找到最小权值的边对应的顶点 v i v_i vi 之后,此时 v i ∈ U v_i \in U vi∈U,判断 V − U V-U V−U 中的顶点 v j v_j vj 到 v i v_i vi 的距离( C [ i ] [ j ] C[i][j] C[i][j])与其到之前访问集的最小距离(也就是当前 L O W C O S T [ j ] LOWCOST[j] LOWCOST[j])的关系,如果 C [ i ] [ j ] < L O W C O S T [ j ] C[i][j]<LOWCOST[j] C[i][j]<LOWCOST[j],那么更新 L O E C O S T [ j ] = C [ i ] [ j ] , C L O S S E T [ j ] = i LOECOST[j]=C[i][j],CLOSSET[j]=i LOECOST[j]=C[i][j],CLOSSET[j]=i。
实现步骤:
- 初始化两个辅助数组 L O W C O S T LOWCOST LOWCOST 和 C L O S S E T CLOSSET CLOSSET;;
- 输出顶点 v s v_s vs ,将顶点 v s v_s vs 加入集合U中;
- 重复执行下列操作
n
−
1
n-1
n−1 次
- 在 L O W C O S T LOWCOST LOWCOST 中选取最短边对应的 V − U V-U V−U 中的顶点 v i v_i vi,取 C L O S S E T CLOSSET CLOSSET 中对应的顶点序号 k k k;
- 输出顶点 k k k 和对应的权值;
- 将顶点 k k k 加入集合U中;
- 调整数组 L O W C O S T LOWCOST LOWCOST 和 C L O S S E T CLOSSET CLOSSET;
下面给出没有做优化的 Prim 算法的伪代码:
void Prim(Costtype C[n+1][n+1], int s) // 给定起点 s
{
costtype LOWCOST[n+1]; int CLOSSET[n+1];
int i, j, k;
costtype min;
for(i=1; i<=n; i++) //初始化数组LOWCOST和数组CLOSSET
{
LOWCOST[i] = C[s][i];
CLOSSET[i] = s;
}
LOWCOST[s] = 0; // 将起始点加入到最小生成树中
for(i = 1; i <= n-1; i++ )
{
min = LOWCOST[1];
k = 1;
for(j = 1; j <= n; j++) { // 在LOWCOST中选最短边,记 CLOSSET 中对应的顶点序号k
if(LOWCOST[j] < min && LOWCOST[j] > 0)
{
min = LOWCOST[j];
k=j;
}
}
cout << “(” << k << “,” << CLOSSET[k] << “)” << end1; // 输出最小生成树的边信息
LOWCOST[k] = 0 ; // 把顶点k加入最小生成树中
for (j = 1; j <= n; j++) { // 调整数组 LOWCOST 和 CLOSSET
if (C[k][j] < LOWCOST[j])
{
LOWCOST[j]=C[k][j];
CLOSSET[j]=k;
}
}
}
} /* 时间复杂度:O(|V|^2)
Prim 算法堆优化
使用最小堆进行优化,可以达到
O
(
∣
E
∣
l
o
g
∣
E
∣
)
O(|E|log|E|)
O(∣E∣log∣E∣) 或
O
(
∣
E
∣
l
o
g
∣
V
∣
)
O(|E|log|V|)
O(∣E∣log∣V∣) 的时间复杂度。不需要用到
L
O
W
C
O
S
T
[
n
]
LOWCOST[n]
LOWCOST[n] 和
C
L
O
S
S
E
T
[
n
]
CLOSSET[n]
CLOSSET[n] 数组,但需要设置
v
i
s
i
t
e
d
[
n
]
visited[n]
visited[n] 数组,表示哪些元素已经被加入到了最小生成树中。
(1)首先将与起点
s
s
s 相连的边加入到最小堆中,并置
v
i
s
i
t
e
d
[
s
]
=
t
r
u
e
visited[s] = true
visited[s]=true 表示顶点
s
s
s 已经加入到了最小生成树中;
(2)每一步从最小堆中弹出权值最小的边,该边有一个顶点
i
i
i 在当前的最小生成树中,另一个顶点
j
j
j 不在最小生成树中,置
v
i
s
i
t
e
d
[
j
]
=
t
r
u
e
visited[j] = true
visited[j]=true 表示将顶点
j
j
j 加入到了最小生成树中;
(3)判断所有与
j
j
j 邻接的边
(
j
,
k
)
(j,k)
(j,k),如果
v
i
s
i
t
e
d
[
k
]
=
=
f
a
l
s
e
visited[k] == false
visited[k]==false,那么将该边也加入到最小堆中;
(4)重复执行前面两个操作,直到所有节点都加入到最小生成树中时停止。
上面堆优化的 P r i m Prim Prim 算法的时间复杂度为 O ( ∣ E ∣ l o g ∣ E ∣ ) O(|E|log|E|) O(∣E∣log∣E∣)。
Kruskal 算法
Kruskal 算法流程
Kruskal 算法是一种巧妙利用并查集来求最小生成树的算法。
算法基本思想:
- 设无向连通网为 G = ( V , E ) G=(V, E) G=(V,E),令 G G G 的最小生成树为 T = ( U , T E ) T=(U,TE) T=(U,TE),其初态为 U = V , T E = { } U=V,TE=\{\} U=V,TE={},即把每个顶点看成一个连通分量;
- 然后,按照边的权值由小到大的顺序,依次考察 G G G 的边集 E E E 中的各条边;
- 若被考察的边连接的是两个不同连通分量,则将此边作为最小生成树的边加入到 T T T 中,同时把两个连通分量连接为一个连通分量;
- 若被考察的边连接的是同一个连通分量,则舍去此边,以免造成回路;
- 如此下去,当 T T T 中的连通分量个数为 1 时,此连通分量便为 G G G 的一棵最小生成树。
Kruskal 算法实现
实现步骤:
- 初始化: U = V ; T E = { } U=V; TE=\{ \} U=V;TE={};
- 循环直到
T
T
T 中的连通分量个数为 1:
- 在E中选择最短边(u,v);
- 如果顶点
u
、
v
u、v
u、v 位于
T
T
T 的两个不同连通分量,则
- 将边 ( u , v ) (u,v) (u,v) 并入 T E TE TE;
- 将这两个连通分量合为一个;
判断两个点是否属于同一个连通分量,以及合并两个连通分量,这就能并查集的优势。
伪代码:
void Kruskal_Min_Tree(EdgeSet edges, int vexnum, int arcnum)
{
int bnf, edf; int parents[100];
Sort(edges); // 按照权值大小排序
for(int i=0; i<vexnum; i++) // 初始化parent[]数组
parents[i]=i;
for(i=0;i<arcnum;i++) {
bnf = Find(edges[i].begin, parents); // 并查集的 find() 函数
edf = Find(edges[i].end, parents);
if(bnf != edf) {
parents[bnf]=edf; // 连通分量合并,没做优化
edges
cout << ‘(’<<vertices[edges[i].begin].data<<‘’, ‘;
cout << vertices[edges[i].end].data<<‘,’<<edges[i].cost<<‘) ’;
cout << endl;
}
}
} /* 时间复杂度:O (|E|*log|E|)