007 基于最小索引堆优化的 Prim 算法求最小生成树

两种 Prim 算法的区别

  • 基于最小索引堆优化的 Prim 算法在堆中最多存储 v-1 条边,即每个节点最多存一条邻边到堆中;而 LazyPrim 算法会将一个节点的多条邻边存进堆中;
  • 由于堆中存储边的数量从 e 变为 v,从而关于堆的操作的时间复杂度从 O(logE) 降为 O(logV);
  • v 是比 e 小的;

存储在文件中的图

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

优化的 Prim 算法 - O(ElogV)

void visit(int v)
  • 访问节点 v,并把 v 所有相邻节点(除了已经纳入 MST 的)的最小权边维护进数组 edgeTo 中;
  • 随着访问的节点越来越多,edgeTo 中有值的节点越来越多,在 edgeTo 中已经有值的节点的值在更新,更新成权更小的边;
核心算法逻辑描述
  • 在探索图的过程中,把探索到的边的权值更新在以节点为横坐标的一维数组中,
  • 从 0 开始探索;
  • 每次探索完了,取出权最小的边纳入 MST;
  • 然后以新纳入的边的另一端开始新的探索;
要点
  • 最小索引堆 ipq带权无向图中的几何意义是:除了已经纳入 MST 的边,已经 visit 到的边;
  • 每次从最小索引堆 ipq 删除的边就是要纳入 MST 的;
  • 最小索引堆 ipq 中存的是遍历到某个程度时,节点当前探索到的权最小的邻边的权值,存储在最小索引堆的 data 数组中,data 数组的每一格表示图的一个节点;
  • data 数组中的值根据大小,以最小堆的结构维护在数组 indexes 中;
  • indexes 数组中元素的个数,对应的是最小堆中元素的个数,对应到图中是涉及到(不是被 visit 到)的点的数目;
package _08._05;

import java.util.Vector;

// 使用优化的Prim算法求图的最小生成树
public class PrimMST {

    private WeightedGraph G;              // 图的引用
    private IndexMinHeap ipq;     // 最小索引堆, 算法辅助数据结构
    private Edge[] edgeTo;        // 访问的点所对应的边, 算法辅助数据结构
    private boolean[] marked;             // 标记数组, 在算法运行过程中标记节点i是否被访问
    private Vector> mst;     // 最小生成树所包含的所有边
    private Number mstWeight;             // 最小生成树的权值

    // 构造函数, 使用Prim算法求图的最小生成树
    public PrimMST(WeightedGraph graph){

        G = graph;
        assert( graph.E() >= 1 );
        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 Vector>();

        // 核心算法 - 优化过的 Prim 算法
        visit(0);
        while( !ipq.isEmpty() ){
            /**
             * v 是已经遍历过的边中权最小的边那头的端点(不属于 MST 的那头),这个端点 v 是将要纳入 MST 的点, 这个端点 v 当前正在考虑的边
             * edgeTo[v] 是要纳入 MST 的边;
             */
            int v = ipq.extractMinIndex();
            // edgeTo[v] 是端点 v 当前正在考虑的边;
            assert( edgeTo[v] != null );
            mst.add( edgeTo[v] );
            visit( v );
        }

        // 计算最小生成树的权值
        mstWeight = mst.elementAt(0).wt();
        for( int i = 1 ; i < mst.size() ; i ++ )
            mstWeight = mstWeight.doubleValue() + mst.elementAt(i).wt().doubleValue();
    }

    /**
     * v 是已经纳入 MST 中的节点;
     * 对 v 的每条边,取到其另一个端点 w;
     * 对还没有纳入 MST 的 w:
     *   如果 w 的任何一条临边是否可以纳入 MST 都没有被考虑过:
     *     先把从 v 指向 w 的边 e 作为 w 当前考虑的边维护进数组 edgeTo 中;
     *     再在最小索引堆中维护 w 节点当前考虑的边的权;
     *   如果已经有临接于 w 的边被考虑过,并且从 v 指向 w 的边 e 的权比当前 w 正在考虑的边的权小:
     *     更新 w 正在考虑的边为 e;
     *     在最小索引堆中更新 w 当前考虑的临边的权;
     *
     * 对于第一个切分中的属于 MST 的唯一的点,比如点 v, 经过 visit(int v),v 的所有临边那头的点都把和 v 之间的边 e 的权视为最小的,随着
     * 图中的点不断的被 visit(int v), 已经维护了最小权边的点会更新其维护的最小权边;
     * @param v
     */
    void visit(int v){

        assert !marked[v];
        marked[v] = true;

        // 将和节点v相连接的未访问的另一端点, 和与之相连接的边, 放入最小堆中
        for( Object item : G.adj(v) ){
            Edge e = (Edge)item;
            int w = e.other(v);
            // 如果边的另一端点未被访问
            if( !marked[w] ){
                // 如果从没有考虑过这个端点, 直接将这个端点和与之相连接的边加入索引堆
                if( edgeTo[w] == null ){
                    edgeTo[w] = e;
                    ipq.insert(w, e.wt());
                }
                // 如果曾经考虑这个端点, 但现在的边比之前考虑的边更短, 则进行替换
                else if( e.wt().compareTo(edgeTo[w].wt()) < 0 ){
                    edgeTo[w] = e;
                    ipq.change(w, e.wt());
                }
            }
        }

    }

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

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


    // 测试 Prim
    public static void main(String[] args) {

        String filename = "testG1.txt";
        int V = 8;

        SparseWeightedGraph g = new SparseWeightedGraph(V, false);
        ReadWeightedGraph readGraph = new ReadWeightedGraph(g, filename);

        // Test Prim MST
        System.out.println("Test Prim MST:");
        PrimMST primMST = new PrimMST(g);
        Vector> mst = primMST.mstEdges();
        for( int i = 0 ; i < mst.size() ; i ++ )
            System.out.println(mst.elementAt(i));
        System.out.println("The MST weight is: " + primMST.result());

        System.out.println();
    }
}

测试

package _08._05;

public class Main {

    // 测试我们实现的两种Prim算法的性能差距
    // 可以看出这一节使用索引堆实现的Prim算法优于上一小节的Lazy Prim算法
    public static void main(String[] args) {

        String filename1 = "src/_08/_05/testG1.txt";
        int V1 = 8;

        // 文件读取
        SparseWeightedGraph g1 = new SparseWeightedGraph(V1, false);
        ReadWeightedGraph readGraph1 = new ReadWeightedGraph(g1, filename1);
        System.out.println( filename1 + " load successfully.");

        long startTime, endTime;

        // Test Prim MST
        System.out.println("Test Prim MST:");

        startTime = System.currentTimeMillis();
        PrimMST primMST1 = new PrimMST(g1);
        endTime = System.currentTimeMillis();
        System.out.println("Test for G1: " + (endTime-startTime) + "ms.");
        System.out.println("Test for G1's mstWeight: " + primMST1.result());

    }
}
输出:
src/_08/_05/testG1.txt load successfully.


Test Prim MST:
Test for G1: 3ms.
Test for G1's mstWeight: 1.81

辅助类

最小索引堆 - IndexMinHeap
package _08._05;

import java.lang.reflect.Array;
import java.util.*;
import java.lang.*;

// 最小索引堆
public class IndexMinHeap {

    protected Item[] data;      // 最小索引堆中的数据
    protected int[] indexes;    // 最小索引堆中的索引, indexes[x] = i 表示索引i在x的位置
    protected int[] reverse;    // 最小索引堆中的反向索引, reverse[i] = x 表示索引i在x的位置
    protected int count;
    protected int capacity;

    // 构造函数, 构造一个空堆, 可容纳capacity个元素
    public IndexMinHeap(int capacity){
        data = (Item[])new Comparable[capacity+1];
        indexes = new int[capacity+1];
        reverse = new int[capacity+1];
        for( int i = 0 ; i <= capacity ; i ++ )
            reverse[i] = 0;

        count = 0;
        this.capacity = capacity;
    }

    // 返回索引堆中的元素个数
    public int size(){
        return count;
    }

    // 返回一个布尔值, 表示索引堆中是否为空
    public boolean isEmpty(){
        return count == 0;
    }

    // 向最小索引堆中插入一个新的元素, 新元素的索引为i, 元素为item
    // 传入的i对用户而言,是从0索引的
    public void insert(int i, Item item){

        assert count + 1 <= capacity;
        assert i + 1 >= 1 && i + 1 <= capacity;

        // 再插入一个新元素前,还需要保证索引i所在的位置是没有元素的。
        assert !contain(i);

        i += 1;
        data[i] = item;
        indexes[count+1] = i;
        reverse[i] = count + 1;
        count ++;

        shiftUp(count);
    }

    // 从最小索引堆中取出堆顶元素, 即索引堆中所存储的最小数据
    public Item extractMin(){
        assert count > 0;

        Item ret = data[indexes[1]];
        swapIndexes( 1 , count );
        reverse[indexes[count]] = 0;
        count --;
        shiftDown(1);

        return ret;
    }

    // 从最小索引堆中取出堆顶元素的索引
    public int extractMinIndex(){
        assert count > 0;

        int ret = indexes[1] - 1;
        swapIndexes( 1 , count );
        reverse[indexes[count]] = 0;
        count --;
        shiftDown(1);

        return ret;
    }

    // 获取最小索引堆中的堆顶元素
    public Item getMin(){
        assert count > 0;
        return data[indexes[1]];
    }

    // 获取最小索引堆中的堆顶元素的索引
    public int getMinIndex(){
        assert count > 0;
        return indexes[1]-1;
    }

    // 看索引i所在的位置是否存在元素
    boolean contain( int i ){
        assert  i + 1 >= 1 && i + 1 <= capacity;
        return reverse[i+1] != 0;
    }

    // 获取最小索引堆中索引为i的元素
    public Item getItem( int i ){
        assert contain(i);
        return data[i+1];
    }

    // 将最小索引堆中索引为i的元素修改为newItem
    public void change( int i , Item newItem ){

        assert contain(i);

        i += 1;
        data[i] = newItem;

        // 有了 reverse 之后,
        // 我们可以非常简单的通过reverse直接定位索引i在indexes中的位置
        shiftUp( reverse[i] );
        shiftDown( reverse[i] );
    }

    // 交换索引堆中的索引i和j
    // 由于有了反向索引reverse数组,
    // indexes数组发生改变以后, 相应的就需要维护reverse数组
    private void swapIndexes(int i, int j){
        int t = indexes[i];
        indexes[i] = indexes[j];
        indexes[j] = t;

        reverse[indexes[i]] = i;
        reverse[indexes[j]] = j;
    }

    //********************
    //* 最小索引堆核心辅助函数
    //********************

    // 索引堆中, 数据之间的比较根据data的大小进行比较, 但实际操作的是索引
    private void shiftUp(int k){

        while( k > 1 && data[indexes[k/2]].compareTo(data[indexes[k]]) > 0 ){
            swapIndexes(k, k/2);
            k /= 2;
        }
    }

    // 索引堆中, 数据之间的比较根据data的大小进行比较, 但实际操作的是索引
    private void shiftDown(int k){

        while( 2*k <= count ){
            int j = 2*k;
            if( j+1 <= count && data[indexes[j+1]].compareTo(data[indexes[j]]) < 0 )
                j ++;

            if( data[indexes[k]].compareTo(data[indexes[j]]) <= 0 )
                break;

            swapIndexes(k, j);
            k = j;
        }
    }

    // 测试 IndexMinHeap
    public static void main(String[] args) {

        int N = 1000000;
        IndexMinHeap indexMinHeap = new IndexMinHeap(N);
        for( int i = 0 ; i < N ; i ++ )
            indexMinHeap.insert( i , (int)(Math.random()*N) );

    }
}

你可能感兴趣的:(007 基于最小索引堆优化的 Prim 算法求最小生成树)