【算法日积月累】15-带权图的最小生成树

【算法日积月累】15-带权图的最小生成树_第1张图片
带权图的最小生成树-1

说明:本章节的讨论基于“带权无向图”,即给定一幅加权无向图,找到它的一棵最小生成树。

最小生成树问题

前提:是一幅连通图(结点不形成环),并且各个边之和最短(连通的路径的总费用最少)。

如果一张图有 个结点,那么相应地,就会有 条边,这 条边连接了 个顶点,形成一棵树,最小生成树上所有的边的权值相加最小。

通常是针对带权无向图,并且是针对连通图,如果不连通,就在各个连通分量上求最小生成树,组成最小生成森林。

图的生成树是它的一棵含有其所有顶点的无环连通子图。一幅加权图的最小生成树(MST)是它的一棵权值(树中所有边的权值之和)最小的生成树。

【算法日积月累】15-带权图的最小生成树_第2张图片
带权图的最小生成树-2

在实际生活中的应用

电缆布线设计。

带权图的数据结构设计、从一个文本文本中读出图的信息到带权图对象

首先我们设计一个边的对象,让它保存一条边的两个结点以及边的权重

下面的设计中,当图是有向图的时候,我们总是认为 a 是边的起点, b 是边的终点。

Java 代码:

// 带权图的边
public class Edge implements Comparable {

    // 边的两个端点的编号(编号是只是为了区分,没有大小关系,即没有比较的含义)
    private int a;
    private int b;
    private Weight weight;

    public Edge(int a, int b, Weight weight) {
        this.a = a;
        this.b = b;
        this.weight = weight;
    }

    public Edge(Edge edge) {
        this.a = edge.a;
        this.b = edge.b;
        this.weight = edge.weight;
    }

    public int v() {
        return a;
    }

    public int w() {
        return b;
    }

    public Weight weight() {
        return weight;
    }

    public int other(int x) {
        assert x == a || x == b;
        return x == a ? b : a;
    }

    // 输出边的信息
    public String toString() {
        return "" + a + "-" + b + ": " + weight;
    }

    @Override
    public int compareTo(Edge that) {
        if (weight.compareTo(that.weight()) < 0) {
            return -1;
        } else if (weight.compareTo(that.weight()) > 0) {
            return 1;
        } else {
            return 0;
        }
    }
}

带权图接口

Java 代码:

// 带权图接口
public interface WeightGraph {

    int V(); // 顶点数

    int E(); // 边数

    // 向图中添加一个边, 权值为 weight
    void addEdge(Edge edge);

    // 验证图中是否有从 v 到 w 的边
    boolean hasEdge(int v, int w);

    // 显示图的信息
    void show();

    // 返回图中一个顶点的所有邻边
    Iterable> adj(int v);
}

实现了带权图接口的稀疏图类和稠密图类、读取一个文本文件的内容到带权图对象

请见:带权图数据结构设计、读取一个文本文件的内容到带权图对象。

本章节使用的带权图文件(testG1.txt)数据如下:

8 16
4 5 .35
4 7 .37
5 7 .28
0 7 .16
1 5 .32
0 4 .38
2 3 .17
1 7 .19
0 2 .26
1 2 .36
1 3 .29
2 7 .34
6 2 .40
3 6 .52
6 0 .58
6 4 .93

MST 算法的理论基础

切分以及切分定理

把图中的结点分为两个部分,称为一个切分(Cut)。如果一个边的两个端点,属于切分(Cut) 不同的两边,这个边称为横切边(Crossing Edge)。

【算法日积月累】15-带权图的最小生成树_第3张图片
绿色的边是横切边。横切边的重要性质是:横切边的边两边的颜色不一样。

切分定理:在一幅加权图中,给定任意切分,所有横切边中权重最小的边一定属于图的最小生成树。

对给定的任意切分都成立,这是非常重要的。有了切分定理,就可以从一个点开始,一点一点扩散,直至找到最小生成树。

理解切分定理的关键是关于树的两条性质:

性质1:一棵树任意连接两个顶点,形成环;

性质2:一棵树任意删除一条边,就会分裂成两棵树。

最小生成树的 Prim 算法的延时实现

从起点出发(并不一定非得是起点,任意一点都可以),每次访问到一个顶点的时候,把所有这个顶点直接相连的、并且还未加入到最小堆中的边加入最小堆,这里的最小堆存储的是待查考的 MST 中的边,即从它们之中选出符合 MST 要求的边,规则是:取出当前最小堆中最小的边,如果它是横切边,就一定是 MST 中的边,如果它不是横切边,就不是 MST 中的边,这一步也是 lazy prim 称之为懒惰的地方。

实现的难点在于:

1、如何判断是不是横切边,与判断是否构成环是否一致?

判断是否构成横切边的方法:判断当前考虑的结点的另一端是否已经被访问过

2、算法停止的条件是什么,是最小堆中的元素已经为空了,还是我们已经找到了 条边?

最小堆

lazy prim 需要借助最小堆,当然,你不用最小堆也可以,那么每一次从待考虑的边中取出最小值,就会比较费时了。

最小堆实现(Java)

lazy prim 算法实现

Java 代码:

import java.util.ArrayList;
import java.util.List;

// LazyPrim :懒惰的 Prim 实现,Prim(普里姆)
// MST:Minimum Spanning Tree,最小生成树
public class LazyPrimMST {

    private WeightGraph G;// 图的引用
    private MinHeap> pq;// 最小堆
    private boolean[] marked; // 标记数组,在算法运行过程中标记结点 i 是否被访问
    private List> mst; // 最小生成树所包含的所有边
    private Number mstWeight;//最小生成树的权值

    // 在构造方法中完成最小生成树的计算
    public LazyPrimMST(WeightGraph graph) {

        // 初始化一些成员变量和辅助的数据结构
        this.G = graph;
        pq = new MinHeap(graph.E());// lazy prim 算法每次考虑的边的条数就是图的最多的边数
        marked = new boolean[this.G.V()];
        mst = new ArrayList<>();

        // lazt prim
        visit(0);
        while (!pq.isEmpty()) { // 每次都拿出考虑的边的最小值,看看它是不是横切边,这就是 lazy prim 称之为 lazy 的原因
            // 使用最小堆找出已经访问的边中权值最小的边
            Edge e = pq.extractMin();
            // 如果这条边的两端都已经访问过了, 则扔掉这条边
            if (marked[e.v()] == marked[e.w()]) {
                continue;
            }
            // 否则,这条边就是最小生成树中的一条边
            mst.add(e);
            
            // 可以认为下面这一步是深度优先遍历
            // 将这条边中还没有被访问过的那个顶点,执行和一开始一样的 visit 操作
            if (!marked[e.v()]) {
                visit(e.v());
            } else {
                visit(e.w());
            }
        }

        // 最小堆中的元素都考虑完以后(根据上面的逻辑,要考虑完那些不是横切边的边)
        this.mstWeight = mst.get(0).weight();
        for (int i = 1; i < mst.size(); i++) {
            this.mstWeight = this.mstWeight.doubleValue() + mst.get(i).weight().doubleValue();
        }
    }

    // 访问一个顶点,只做一件事情,将和这个顶点直接相连的还未加入最小堆的边加入最小堆
    private void visit(int v) {
        assert !marked[v];
        marked[v] = true;
        for (Edge e : this.G.adj(v)) {
            // 【注意】技巧在这里:只要是另一个端点还没有被标记过,那么就表示这条边还未加入到最小堆中
            if (!marked[e.other(v)]) {
                pq.insert(e);
            }
        }
    }

    // 返回最小生成树的所有边
    public List> mstEdges() {
        return mst;
    }

    // 返回最小生成树的权值
    public Number result() {
        return mstWeight;
    }
}

测试方法:

Java 代码:

import java.util.List;

public class LazyPrimMSTTest {

    public static void main(String[] args) {
        String filename = "testG1.txt";
        int v = 8;
        // 无权图
        SpareWeightedGraph g = new SpareWeightedGraph<>(v, false);
        ReadWeightedGraphUtil readWeightedGraphUtil = new ReadWeightedGraphUtil(g, filename);
        g.show();
        System.out.println("最小生成树的 Prim 算法的延时实现:");
        LazyPrimMST lazyPrimMST = new LazyPrimMST<>(g);
        List> mst = lazyPrimMST.mstEdges();
        mst.forEach(System.out::println);
    }
}

运行结果:

【算法日积月累】15-带权图的最小生成树_第4张图片
带权图的最小生成树-3

Prim 算法的即时实现

我们要借助最小索引堆完成 Prim 算法。

最小索引堆的实现:最小索引堆(Java 代码)。

Java 代码:

import java.util.ArrayList;
import java.util.List;

// Prim 算法的即时实现
public class PrimMST {

    private WeightGraph G;// 图的引用
    private IndexMinHeap ipq;// 最小索引堆
    private boolean[] marked;

    private Edge[] edgeTo;// 访问的点所对应的边
    private List> mst; // 最小生成树所包含的所有边
    private Number mstWeight;//最小生成树的权值

    public PrimMST(WeightGraph graph) {
        G = graph;
        ipq = new IndexMinHeap<>(graph.V());

        // 初始化辅助的数据结构和一些成员变量
        marked = new boolean[G.V()];
        edgeTo = new Edge[G.V()];

        for (int i = 0; i < G.V(); i++) {
            marked[i] = false;
            edgeTo[i] = null;
        }

        mst = new ArrayList<>();

        // Prim
        visit(0);
        while (!ipq.isEmpty()){
            int v = ipq.extractMinIndex();
            assert edgeTo[v]!=null;
            mst.add(edgeTo[v]);
            visit(v);
        }
        // 最小堆中的元素都考虑完以后(根据上面的逻辑,要考虑完那些不是横切边的边)
        this.mstWeight = mst.get(0).weight();
        for (int i = 1; i < mst.size(); i++) {
            this.mstWeight = this.mstWeight.doubleValue() + mst.get(i).weight().doubleValue();
        }
    }

    private void visit(int v) {
        assert !marked[v];
        marked[v]=true;

        for(Edge e: this.G.adj(v)){
            int w = e.other(v);
            // 如果顶点的另一个端点还没有访问过,那么这条边就可以加入索引堆
            if(!marked[w]){
                 if(edgeTo[w]==null){
                     edgeTo[w]= e;
                     ipq.insert(w,e.weight());
                 }else if(e.weight().compareTo(edgeTo[w].weight())<0){
                     edgeTo[w]= e;
                     ipq.change(w,e.weight());
                 }
            }
        }
    }

    // 返回最小生成树的所有边
    public List> mstEdges() {
        return mst;
    }

    // 返回最小生成树的权值
    public Number result() {
        return mstWeight;
    }
}

测试方法:

import java.util.List;

public class PrimMSTTest {

    public static void main(String[] args) {
        String filename = "testG1.txt";
        int v = 8;
        // 无权图
        SpareWeightedGraph g = new SpareWeightedGraph<>(v, false);
        ReadWeightedGraphUtil readWeightedGraphUtil = new ReadWeightedGraphUtil(g, filename);
        g.show();
        System.out.println("最小生成树的 Prim 算法的即时实现:");
        PrimMST primMST = new PrimMST<>(g);
        List> mst = primMST.mstEdges();
        mst.forEach(System.out::println);
    }
}

运行结果:

【算法日积月累】15-带权图的最小生成树_第5张图片
带权图的最小生成树-4

使用 Kruskal(克鲁斯卡尔)算法计算带权图的最小生成树

需要使用的辅助的数据结构有:

1、最小堆;

2、并查集。

Java 代码:

import java.util.ArrayList;
import java.util.List;

// Kruskal(克鲁斯卡尔)算法
public class KruskalMST {

    private List> mst;
    private Number mstWeight;

    // 使用 Kruskal 算法计算带权图的最小生成树
    public KruskalMST(WeightGraph graph) {

        mst = new ArrayList<>();

        // 马上我们就会看到,Kruskal 算法会把所有的边都考虑进去
        MinHeap> pq = new MinHeap<>(graph.E());
        // 把所有的边都加入到这个最小堆中
        for (int i = 0; i < graph.V(); i++) {
            for (Edge e : graph.adj(i)) {
                // 为了防止重复加入边
                // 为了防止重复加入边
                // 为了防止重复加入边
                if (e.v() < e.w()) {
                    pq.insert(e);
                }
            }
        }

        // 为了判断顶点是否相连接,所以应该开辟顶点数规模的并查集
        UnionFind uf = new UnionFind(graph.V());

        while (!pq.isEmpty() && mst.size() < graph.V()-1){// 凑够数了,就不必进行下去了

            // 从最小堆中拿出当前未考虑到的最小权重的边
            Edge e = pq.extractMin();

            // 如果这条边的两个端点是连通的,说明加入这条边会产生环,那么它肯定不是 MST 中的一条边
            if(uf.connected(e.v(),e.w())){
                continue;
            }

            // 如果这条边的两个端点不是连通的,这条边就是 MST 中的一条边,加入之后,不要忘记标记两个结点相连
            mst.add(e);
            uf.union(e.v(),e.w());
        }
        this.mstWeight = mst.get(0).weight();
        for (int i = 1; i < mst.size(); i++) {
            this.mstWeight = this.mstWeight.doubleValue() + mst.get(i).weight().doubleValue();
        }
    }

    public List> mstEdges() {
        return mst;
    }

    public Number result() {
        return mstWeight;
    }

}

测试方法:

Java 代码:

import java.util.List;

public class KruskalMSTTest {

    public static void main(String[] args) {
        String filename = "testG1.txt";
        int v = 8;
        // 无权图
        SpareWeightedGraph g = new SpareWeightedGraph<>(v, false);
        ReadWeightedGraphUtil readWeightedGraphUtil = new ReadWeightedGraphUtil(g, filename);
        g.show();
        System.out.println("最小生成树的 Kruskal 算法实现:");
        KruskalMST kruskalMST = new KruskalMST<>(g);
        List> mst = kruskalMST.mstEdges();
        mst.forEach(System.out::println);
    }
}

运行结果:

【算法日积月累】15-带权图的最小生成树_第6张图片
带权图的最小生成树-5

本文源代码

Python:代码文件夹,Java:代码文件夹。

参考资料

本章节中的所有专业插图都来自《算法(第 4 版)》这本书的配套网站。

(本节完)

你可能感兴趣的:(【算法日积月累】15-带权图的最小生成树)