最小生成树问题

12 篇文章 0 订阅
5 篇文章 4 订阅

最小生成树(Minimum Span Tree)

  • 针对带权无向图
  • 针对连通图
    在这里插入图片描述

切分

把途中的结点分为两部分,成为一个切分(Cut)
在这里插入图片描述
如果一个边的两个端点,属于切分不同的两边,这个边称为横切边(Crossing Edge)

切分定理

给定任意切分,横切边中权值最小的边必然属于最小生成树。
在这里插入图片描述

Lazy Prim

在这里插入图片描述
在这里插入图片描述

最小堆


/*
 * Created by wxn
 * 2018/12/16 3:16
 */

/**
 * 最小堆
 */
public class MinHeap<T extends Comparable<T>> {

    private T data[];
    private Integer count;
    private Integer capacity;

    public MinHeap(Integer capacity) {
	this.capacity = capacity;
	data = (T[]) new Comparable[capacity + 1];
	count = 0;
    }

    public int size() {
	return count;
    }

    public boolean isEmpty() {
	return count == 0;
    }

    public void insert(T item) {
	assert count < capacity;
	data[count + 1] = item;
	count++;
	shiftUp(count);
    }

    public T extractMin() {
	assert count > 0;
	T ret = data[1];
	swap(1, count);
	count--;
	shiftDown(1);
	return ret;
    }

    private void shiftDown(int k) {
	while (k * 2 <= count) {
	    int j = k * 2;
	    if (j + 1 <= count && data[j + 1].compareTo(data[j]) < 0) {
		j++;
	    }
	    if (data[k].compareTo(data[j]) > 0) {
		swap(j, k);
		k = j;
	    } else {
		break;
	    }
	}
    }

    private void shiftUp(int k) {
	while (k > 1 && data[k / 2].compareTo(data[k]) > 0) {
	    swap(k, k / 2);
	    k /= 2;
	}
    }

    // 交换堆中索引为i和j的两个元素
    private void swap(int i, int j) {
	T t = data[i];
	data[i] = data[j];
	data[j] = t;
    }


    public static void main(String args[]) {
	Integer[] arr = new Integer[20];
	for (int i = 0; i < arr.length; i++) {
	    arr[i] = (int) (Math.random() * 100);
	}
	MinHeap<Integer> minHeap = new MinHeap<>(arr.length);
	for (int i = 0; i < arr.length; i++) {
	    minHeap.insert(arr[i]);
	}
	for (int i = 0; i < arr.length; i++) {
	    System.out.print(minHeap.extractMin() + " ");
	}
    }

}

LazyPrimMst


/*
 * Created by wxn
 * 2018/12/16 3:43
 */

import java.util.Vector;

/**
 * LazyPrim最小生成树
 */
public class LazyPrimMst<Weight extends Number & Comparable<Weight>> {

    private WeightGraph<Weight> graph;  // 图的引用
    private MinHeap<Edge<Weight>> minHeap;    // 最小堆, 算法辅助数据结构
    private boolean[] mark;             // 标记数组, 在算法运行过程中标记节点i是否被访问
    private Vector<Edge<Weight>>mst;    // 最小生成树所包含的所有边
    private Number mstWeight;           // 最小生成树的权值

    public LazyPrimMst(WeightGraph<Weight> graph){
        this.graph = graph;
        minHeap = new MinHeap<>(graph.E());
        mark = new boolean[graph.V()];
        mst = new Vector<>();

        visit(0);
        while (!minHeap.isEmpty()){
            // 使用最小堆找出已经访问的边中权值最小的边
            Edge<Weight> e = minHeap.extractMin();
            // 如果这条边的两端都已经访问过了, 则扔掉这条边
            if (mark[e.v()]==mark[e.w()]){
                continue;
            }
            // 否则, 这条边则应该存在在最小生成树中
            mst.add(e);
            // 访问和这条边连接的还没有被访问过的节点
            if (!mark[e.v()]){
                visit(e.v());
            }else {
                visit(e.w());
            }
        }

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

    }

    private void visit(int v) {
        assert !mark[v];
        mark[v] = true;
        //将所有和节点v相连的未访问的边放入最小堆中
        for (Edge<Weight> e : graph.adj(v)) {
            if (!mark[e.other(v)]){
                minHeap.insert(e);
            }
        }
    }


    public static void main(String args[]) {
        WeightGraph<Double>graph = new SparseWeightedGraph<>(8,false);
        String filename = "TestG1.txt";
        ReadWeightedGraph readWeightedGraph = new ReadWeightedGraph(graph,filename);
        graph.show();

        LazyPrimMst<Double> lazyPrimMst = new LazyPrimMst<>(graph);
        for (Edge<Double> edge : lazyPrimMst.mst) {
            System.out.println(edge);
        }
        System.out.println("total weight: " + lazyPrimMst.mstWeight);
    }


}

运行结果

在这里插入图片描述
与预期相符
在这里插入图片描述

优化的Prim算法


/*
 * Created by wxn
 * 2018/12/16 21:22
 */


/**
 * 最小索引堆
 */
public class IndexMinHeap<T extends Comparable<T>> {
    private T[] data;                //最小索引堆中的数据
    private int[] indexes;        //最小索引堆中的索引,indexes[x]=i表示索引i在x的位置
    private int[] reverse;        //最小索引堆中的反向索引,reverse[i]=x表示索引i在x的位置
    private int count;
    private int capacity;

    public IndexMinHeap(int capacity) {
	this.capacity = capacity;
	data = (T[]) 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;
    }

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

    //向最小索引堆中插入一个新元素,索引为i 元素为item
    //传入的i对用户而言是从0索引的
    public void insert(int i, T item) {
	assert count + 1 <= capacity;
	assert i + 1 >= 1 && i + 1 <= capacity;
	assert !contain(i);
	i++;
	data[i] = item;
	indexes[count+1] = i;
	reverse[i] = count+1;
	count++;
	shiftUp(count);
    }

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

	T 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 T getMin(){
	assert count > 0;
	return data[indexes[1]];
    }

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

    // 索引堆中, 数据之间的比较根据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;
	}
    }

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

    // 交换索引堆中的索引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;
    }
    // 看索引i所在的位置是否存在元素
    private boolean contain(int i) {
	assert  i + 1 >= 1 && i + 1 <= capacity;
	return reverse[i+1] != 0;
    }
    // 获取最小索引堆中索引为i的元素
    public T getItem( int i ){
	assert contain(i);
	return data[i+1];
    }

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

	assert contain(i);

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

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

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

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

    }
}

/*
 * Created by wxn
 * 2018/12/16 21:39
 */

import java.util.Vector;

// 使用优化的Prim算法求图的最小生成树
public class PrimMst<Weight extends Number & Comparable<Weight>> {
    private WeightGraph graph;			//图的引用
    private IndexMinHeap<Weight> indexMinHeap;	//最小索引堆
    private Edge<Weight>[] edgeTo;		//访问的点所对应的边
    private boolean[] marked;			//标记数组,在算法运行过程中标记节点i是否被访问
    private Vector<Edge<Weight>> mst;		//最小生成树所包含的所右边
    private Number mstWeight;			//最小生成树的权值

    public PrimMst(WeightGraph graph) {
	this.graph = graph;
	assert graph.E()>=1;
	indexMinHeap = new IndexMinHeap<>(graph.V());
	marked = new boolean[graph.V()];
	edgeTo = new Edge[graph.V()];
	for (int i = 0; i < graph.V(); i++) {
	    marked[i] = false;
	    edgeTo[i] = null;
	}
	mst = new Vector<>();
	
	//Prim
	visit(0);
	while (!indexMinHeap.isEmpty()){
	    // 使用最小索引堆找出已经访问的边中权值最小的边
	    // 最小索引堆中存储的是点的索引, 通过点的索引找到相对应的边
	    int v = indexMinHeap.extractMinIndex();
	    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
    private void visit(int v) {
        assert !marked[v];
        marked[v] = true;

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

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

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

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

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

	// Test Prim MST
	System.out.println("Test Prim MST:");
	PrimMst<Double> primMST = new PrimMst<>(g);
	Vector<Edge<Double>> 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();
    }
}

运行结果

在这里插入图片描述
结果与之前相同

Kruskal Mst

原理

在这里插入图片描述

  • 将图的所有边按权值从小到大排序,依次遍历这些边。如果将遍历到的边加入到MST中没有形成环,那么这条边就是最小生成树的一条边。
  • 如何判断是否形成环?
    利用并查集(UnionFind):如果这条边的两个顶点在并查集中相连的话,那么就会形成环。

代码


/*
 * Created by wxn
 * 2018/12/16 22:32
 */

/**
 * 并查集
 */
public class UnionFind {

    // rank[i]表示以i为根的集合所表示的树的层数
    // 在后续的代码中, 我们并不会维护rank的语意, 也就是rank的值在路径压缩的过程中, 有可能不在是树的层数值
    // 这也是我们的rank不叫height或者depth的原因, 他只是作为比较的一个标准
    private int[] rank;
    private int[] parent;        //parent[i]表示第i个元素所指向的父节点
    private int count;

    public UnionFind(int count) {
	this.count = count;
	rank = new int[count];
	parent = new int[count];
	for (int i = 0; i < count; i++) {
	    parent[i] = i;
	    rank[i] = 0;
	}
    }

    // 查找过程, 查找元素p所对应的集合编号
    // O(h)复杂度, h为树的高度
    int find(int p) {
	assert p >= 0 && p < count;
	//path compression
	while (p != parent[p]) {
	    parent[p] = parent[parent[p]];
	    p = parent[p];
	}
	return p;
    }

    // 查看元素p和元素q是否所属一个集合
    // O(h)复杂度, h为树的高度
    boolean isConnected(int p, int q) {
	return find(p) == find(q);
    }

    // 合并元素p和元素q所属的集合
    // O(h)复杂度, h为树的高度
    void unionElements(int p, int q) {
        int pRoot = find(p);
        int qRoot = find(q);
        if (pRoot==qRoot){
            return;
	}
	// 根据两个元素所在树的元素个数不同判断合并方向
	// 将元素个数少的集合合并到元素个数多的集合上
	if (rank[pRoot]<rank[qRoot]){
	    parent[pRoot] = qRoot;
	}else if (rank[pRoot]>rank[qRoot]){
	    parent[qRoot] = pRoot;
	}
	else {	//rank[pRoot]=rank[qRoot]
	    parent[pRoot] = qRoot;
	    rank[qRoot]++;
	}
    }
}

/*
 * Created by wxn
 * 2018/12/16 22:43
 */

import java.util.Vector;

/**
 * Kruskal算法求最小生成树
 */
public class KruskalMst<Weight extends Number & Comparable<Weight>> {

    private Vector<Edge<Weight>> mst;        //最小生成树所包含的所右边
    private Number mstWeight;                //最小生成树的权值

    public KruskalMst(WeightGraph graph) {

	mst = new Vector<>();
	//将图中所有边存放到一个最小堆中
	MinHeap<Edge<Weight>> minHeap = new MinHeap<>(graph.E());
	for (int i = 0; i < graph.V(); i++) {
	    for (Object item : graph.adj(i)) {
		Edge<Weight> e = (Edge<Weight>) item;
		if (e.v() < e.w()) {
		    minHeap.insert(e);
		}
	    }
	}
	//创建一个并查集,查看已经访问的节点的联通情况
	UnionFind uf = new UnionFind(graph.V());
	while (!minHeap.isEmpty() && mst.size() < graph.V() - 1) {
	    //从最小堆中一次从小到大取出所有边
	    Edge<Weight> e = minHeap.extractMin();
	    // 如果该边的两个端点是联通的, 说明加入这条边将产生环, 扔掉这条边
	    if (uf.isConnected(e.v(), e.w())) {
		continue;
	    }
	    // 否则, 将这条边添加进最小生成树, 同时标记边的两个端点联通
	    mst.add(e);
	    uf.unionElements(e.v(), e.w());
	}
	// 计算最小生成树的权值
	mstWeight = mst.elementAt(0).wt();
	for (int i = 1; i < mst.size(); i++)
	    mstWeight = mstWeight.doubleValue() + mst.elementAt(i).wt().doubleValue();
    }
    // 返回最小生成树的所有边
    Vector<Edge<Weight>> mstEdges(){
	return mst;
    }

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


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

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

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

	// Test Kruskal
	System.out.println("Test Kruskal:");
	KruskalMst<Double> kruskalMST = new KruskalMst<>(g);
	Vector<Edge<Double>> mst = kruskalMST.mstEdges();
	for( int i = 0 ; i < mst.size() ; i ++ )
	    System.out.println(mst.elementAt(i));
	System.out.println("The MST weight is: " + kruskalMST.result());

	System.out.println();
    }
}

运行结果

在这里插入图片描述

拓展:Vyssotsky’ s Algorithm

  • 将边逐渐地添加到生成树中,一旦形成环,删除环中权值最大的边。

时间复杂度

  • LazyPrimMst: O(ElogE)
  • PrimMst: O(ElogV)
  • Kruskal: O(ElogE)

我的github仓库:
https://github.com/649733108/Mook-Play-with-Algorithms/tree/master/WeightedGraph/src
欢迎骚扰!

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值