第四章 图(六) - 《算法》读书笔记
目录
第四章 图(六)
4.3 最小生成树
- 加权图是每条边关联一个权值或是成本的图模型
定义:图的生成树是它的一颗含有其所有顶点的无环连通子图。一幅加权图的最小生成树(MST)是它的一颗权值(图中所有边的权值之和)最小的生成树。
- 一些约定:
- 只考虑连通图
- 边的权重不一定表示距离
- 边的权重可能是0或者负数
- 所有边的权重都各不相同
4.3.1 原理
- 树的两个重要性质:
- 用一条边连接树中的任意两个顶点都会产生一个新的环
- 从树中删去一条边将会得到两棵独立的树
4.3.1.1 切分定理
定义:图的一种切分是将图的所有顶点分为两个非空且不重复的两个集合。横切边是一条连接两个属于不同集合的顶点的边。
切分定理:在一幅加权图中,给定任意的切分,它的横切边中的权重最小者必然属于图的最小生成树。
4.3.1.2 贪心算法
最小生成树的贪心算法:下面这种算法会将含有V个顶点的任意加权连通图中属于最小生成树的边标记为黑色:初始状态下所有边均为灰色,找到一种切分,它产生的横切边均不为黑色,将它权重最小的横切边标记为黑色,如此反复,直到标记了V-1条黑色边为止。
- 不同贪心算法的区别在于保存切分和判定权重最小的横切边的方式,但都是以上算法的特殊情况
4.3.4 Prim算法
- Prim算法的每一步都会为一棵生长中的树添加一条边,每次将下一条,连接树中的顶点,与不在树中的顶点,且权重最小的边,加入树中(即由树中的顶点所定义的切分中的一条横切边)
Prim算法能够得到任意加权连通图的最小生成树。
4.3.4.1 数据结构
- 顶点:使用一个由顶点索引的布尔数据marked[],如果顶点v在树中,marked[v] = true
- 边:选择以下两种数据结构之一:
- 一条队列mst来保存最小生成树中的边
- 一个由顶点索引的Edge对象的数组edgeTo[],其中edgeTo[v]为将v连接到树中的Edge对象
- 横切边:使用一条优先队列MinPQ<Edge>来根据权重比较所有边
4.3.4.2 维护横切边的集合
- 向树中添加一条边后,需要添加对应的顶点
- 在优先队列中,需要加入连接这个顶点和其他所有不在树中的顶点的边,同时这个顶点和其他已经在树中的顶点的边都失效了
- Prim算法的即时实现可以将失效的边从优先队列中删掉,但我们先实现一种延时实现,将失效的边先留在队列中,等到要删除它们的时候再检查边的有效性
4.3.4.3 实现
public class LazyPrimMST{
private boolean[] marked;
private Queue<Edge> mst;
private MinPQ<Edge> pq;
public LazyPrimMST(EdgeWeightedGraph G){
pq = new MinPQ<Edge>();
marked = new boolean[G.V()];
mst = new Queue<Edge>();
visit(G, 0);
while(!pq.isEmpty()){
Edge e = pq.delMin();
int v = e.either(), w = e.other(v);
if(marked[v] && marked[w]) continue;
mst.enqueue(e);
if(!marked[v]) visit(G, v);
if(!marked[w]) visit(G, w);
}
}
//在树中添加顶点v,将与其关联的所有未失效的边加入优先队列
private void visit(EdgeWeightedGraph G, int v){
marked[v] = true;
for(Edge e : G.adj(v))
if(!marked[e.other(v)])
pq.insert(e);
}
}
4.3.4.4 运行时间
Prim算法的延时实现计算一幅含有V个顶点和E条边的连通加权无向图的最小生成树所需的空间与E成正比,所需的时间与ElogE成正比(最坏情况)。
4.3.5 Prim算法的即时实现
- 我们只会在优先队列中保存每个非树顶点w的一条边:将它与树中的顶点连接起来权重最小的那条边
- 将w和树的顶点连接起来的其他权重较大的边迟早都会失效
- 具体实现如下:
public class PrimMST{
private Edge[] edgeTo; //距离树最近的边
private double[] distTo; //distTo[w] = edgeTo[w].weight()
private boolean marked;
private IndexMinPQ<Double> pq; //索引优先队列保存有效的横切边
public PrimMST(EdgeWeightedGraph G){
edgeTo = new Edge[G.V()];
distTo = new double[G.V()];
marked = new boolean[G.V()];
for(int v = 0; v < G.V(); v++)
distTo[v] = Double.POSITIVE_INFINITY;
pq = new IndexMinPQ<Double>(G.V());
distTo[0] = 0.0;
pq.insert(0, 0.0); //用顶点0和权重0初始化pq, int v
while(!pq.isEmpty())
visit(G, pq.delMin()); //将最近的顶点添加到树中
}
private void visit(EdgeWeightedGraph G, int v){
marked[v] = true;
for(Edge e : G.adj(v)){
int w = e.other(v);
if(marked[w]) continue; //v-w失效
if(e.weight() < distTo[w]){
edgeTo[w] = e;
distTo[w] = e.weight();
if(pq.contains(w)) pq.change(w, distTo[w]);
else pq.insert(w, distTo[w]);
}
}
}
}
-
PrimMST中的edgeTo[]和distTo[]有如下性质:
- 如果顶点v不在树中,但至少有一条边和树相连,那么edgeTo[v]是将v和树连接的最短边,distTo[v]为这条边的权重
- 所有这类顶点v都保存在一条索引优先队列中,索引v关联的值是edgeTo[v]的边的权重
-
这些性质的关键点在于优先队列中的最小键即是权重最小的横切边的权重,而和它相关联的顶点v就是下一个将被添加到树中的顶点
Prim算法的即时实现计算一幅含有V个顶点和E条边的连通加权无向图的最小生成树所需的空间和V成正比,所需的时间和ElogV成正比(最坏情况)。
4.3.6 Kruskal算法
- Kruskal算法按照边的权重顺序(从小到大),加入最小生成树中,加入的边不会与已经加入的边构成环,直到树中含有V-1条边为止
Kruskal算法能够计算任意加权连通图的最小生成树。
- 具体实现如下:
public class KruskalMST{
private Queue<Edge> mst;
public KruskalMST(EdgeWeightedGraph G){
mst = new Queue<Edge>();
MinPQ<Edge> pq = new MinPQ<Edge>();
for(Edge e : G.edges())
pq.insert(e);
UF uf = new UF(G.V()); //union_find数据结构
while(!pq.isEmpty() && mst.size() < G.V() - 1){
Edge e = pq.delMin();
int v = e.either(), w = e.other();
if(uf.connected(v, w)) continue;
uf.union(v, w);
mst.enqueue(e);
}
}
}
Kruskal算法的计算一幅含有V个顶点和E条边的连通加权无向图的最小生成树所需的空间和E成正比,所需的时间和ElogE成正比(最坏情况)。
- 使用路径压缩的加权quick-union算法可以将所有节点直接链接到根结点,connected方法的运行时间是常数级别,union方法在均摊后的运行时间也是常数级别的,因此E次connected和V次union操作所需的时间成本,与ElogE的总时间的增长数量级相比,可以忽略不计。

浙公网安备 33010602011771号