美文网首页
最小生成树问题——Kruskal算法实现

最小生成树问题——Kruskal算法实现

作者: 进击的NULL | 来源:发表于2019-01-10 15:32 被阅读0次

    问题介绍

    有一天我看到这么一个描述:古时候的镖局(相当于现在的快递公司)要押镖,然后有一张地图。地图上面清晰的标记了从A城市出发到B城市的每一条线路,所经过的每个城市。但是由于古时候绿林好汉太多了(还是社会主义好啊,扯远了~~~),两两城市之间绿林好汉收取的保护费是不一样的,所以这就导致压镖成本不同,那么怎样设计镖局所在地,才能使镖局能到达所有城市且打点绿林好汉的成本最少。问题描述完了,有没有觉得蛮有意思呢?那么经过模型化,其实上面问题就转化为:给你一个连通图,让你求它的最小生成树,最终满足图中两顶点之间有且仅有一条路径能到达,并且所有边的权重之后最小。如下图所示。


    压镖路线开销图.png

    问题分析

    从问题描述中,为了满足问题需求,我们大致可以分析出以下几点:

    • 要让n个顶点连通(无向图,两两之间互通),至少需要n -1 条边;
    • 要让所有权重之后最小,应该从最小边开始逐个添加,直到添加满n -1条边为止;
    • 怎样确定添加的一条边是否满足“最小”的概念,有一点就是这条边不能形成环,也就是边两顶点不能已经是在“一颗树上”的;
    • 怎样判断两个顶点是否“在一棵树上”?用深度优先、广度优先都可以,但是时间复杂度比较高,所以我选择用并查集。

    开始动手写代码

    先上代码:

    package disjointset;
    
    import java.util.Scanner;
    
    /**
     * 最小生成树问题
     * @author XZP
     * 一组测试用例
     6 9
    2 4 11
    3 5 13
    4 6 3
    5 6 4
    2 3 6
    4 5 7
    1 2 1
    3 4 9
    1 3 2
     */
    public class LongmenExpress {
    
        public static void main(String[] args) {
            int i = 0; // 循环计数变量
            int j = 1;
            int a, b, c; // a城到b城的花销c
            int count = 0; // 已经找到边的条数
            int sum = 0; // 最小生成树的开销总和
            Scanner sc = new Scanner(System.in);
            int n = sc.nextInt(); // 城市总数
            int m = sc.nextInt(); // 道路总数
            Cost[] costs = new Cost[m + 1]; // 开销数组
            int[] book = new int [m + 1]; // 存储costs数组中对应的边是否被纳入最小生成树
            int[] f = new int[n + 1]; // 存储每个节点的父节点
            initF(f, n);
            for (i = 1; i <= m; i++) {
                a = sc.nextInt();
                b = sc.nextInt();
                c = sc.nextInt();
                costs[i] = new Cost(a, b, c); // 将costs的每个索引值指向一个Cost对象
    //          merge(f, a, b);
            }
            sort(costs, 1, m); // 根据消耗大小将路线按从小到大排序
            for (; j <= m; j++  ) {
                a = costs[j].getA();
                b = costs[j].getB();
                c = costs[j].getC();
                // 判断两个点是否已经在一个阵营
                if (getF(a, f) != getF(b, f)) { // 这个地方注意一下,不要用f[a] != f[b]来判断,getF会根据调用栈,迭代更新,但是f[x]则不会,所以要用getF()函数判断
                    merge(f, a, b);
                    book[j] = 1;
                    sum += c;
                    count++; // 找到一条有效边即加一
                }
                if (count == n-1) {
                    break;
                }
            }
            // 输出
            System.out.println("最小生成树总花销为:" + sum);
            for (i = 1; i <= m; i++) {
                if (book[i] == 1) {
                    System.out.println("节点 " +costs[i].getA() +  " 到节点 " + costs[i].getB() + ", 这趟花销 " +costs[i].getC());
                }
            }
        }
        public static void merge(int[] f, int a, int b) {
            int t1, t2;
            // 获取两个节点的父节点
            t1 = getF(a, f);
            t2 = getF(b, f); // 懂了,从这里找到b的根节点,这就是所谓的擒贼先擒王!
            if (t1 != t2) { // 当两个节点不在一个阵营时,要根据靠左原则和擒贼先擒王原则将对应的节点信息更新
                f[t2] = t1; // 曾经自己写错过!!!!!错在忽视了擒贼擒王的道理,更新的值不是t1而是a
            }
        }
        /**
         * 递归寻找节点x的父节点
         * @param x
         * @param f
         * @return
         */
        public static int getF(int x, int[] f) {
            if (f[x] == x) {
                return x;
            } else {
                f[x] = getF(f[x], f);
                return f[x];
            }
        }
        /**
         * 使用快速排序将costs数组进行排序(快排时间复杂度O(MlogM))
         * @param costs
         * @param low
         * @param high
         */
        public static void sort(Cost[] costs, int low, int high) {
            if (low > high) {
                return;
            }
            int i, j ;
            Cost temp;
            Cost t;
            temp = costs[low];
            i = low;
            j = high;
            while (i != j) {
                while(costs[j].getC() >= temp.getC() && i < j) {
                    j--;
                    }
                while (costs[i].getC() <= temp.getC() && i < j) {
                    i++;
                }
                if (i < j) { // 交换两个对象的引用在数组中的位置
                    t = costs[i];
                    costs[i] = costs[j];
                    costs[j] = t;
                }
            }
            // 最终将基准数归为
            costs[low] = costs[i];
            costs[i] = temp;
            sort(costs, low, i -1);
            sort(costs, i + 1, high);
        }
        /**
         * 初始化每个节点的父节点为自身
         * @param f
         * @param n
         */
        public static void initF(int[] f, int n) {
            for (int i =1; i <= n; i++) {
                f[i] = i;
            }
        }
    
    }
    // 定义一个开销结构体对象
    class Cost {
        public int getA() {
            return a;
        }
        public int getB() {
            return b;
        }
        public int getC() {
            return c;
        }
        private int a; // 城市a
        private int b; // 城市b
        private int c; // a、b两城市之间的开销
        public Cost(int a, int b, int c) {
            this.a = a;
            this.b = b;
            this.c = c;
        }
    }
    
    

    首先解决两顶点“在同一颗树上”的问题

    我用了一个数组f[],最开始时,这个数组中的值初始化为下标索引本身,表示编号i的顶点它最初只跟自己一个阵营,就是i。随着边的关系确定,比如输入

    a b c(笔者注:c输入的是a,b两城之间的开销)

    然后将f[b] = a处理,这样做的目的是将a、b归为一个阵营,并让b此时的根节点指向a,表示现在b跟你a混了,你当我老大吧。这样做有一个好处就是可以用递归实现一个“归顺效果”。比如有个A想征服b,输入

    A b d (笔者注:d输入的是a,b两城之间的开销)

    那么b不用管那么多,只需要告诉A:我的老大是a,你要想我的f[b]指向你,那首先你要找我老大a,让它f[a]指向你,然后递归到我这里。反过来,下次别人问我老大是谁。b就先问a老大是谁,然后a在告诉b,我的老大已经是A(不是它自己了!!!),你也改一下你(这里是b)的f[b]值。
    以上差不多我将如何确定两个人已经是一个阵营(一棵树上)的问题理了一遍,详细可以看看代码中的merge()方法和getF()方法,递归迭代的解决了以上问题。注意我注释的一些细节之处,因为我最开始在那里翻过车。

    怎样满足没有环?

    上面介绍了如何判断两个顶点是否在一个阵营(其实就是f[]矩阵下标对应的值是不是同一个老大),那么只要两者不是一个阵营中,就将满足的这条边加入最小生成树。我代码中是用了一个book[m + 1]标记矩阵来标识,当满足条件将这个数组对应下标值置为1,表示第i条边满足最小生成树的条件,加进去,算作一条有效边!

    怎样选择才能使被选出的前n -1权重和最小?

    这里,我就想,既然是要选n -1条边,那肯定我们都是从权重最小的边开始选,所以我首先将边根据开销用快排进行了从小到大的排序。为了便于存储,我用了类似C/C++中结构体的对象(哈哈哈哈,这样用有时候要注意下),写了一个Cost类存储两个顶点信息和开销信息,并生成了一个读取器。

    算法分析

    具体遇到的两个小坑以及算法复杂度可以从代码中得到,在此不再赘述。

    相关文章

      网友评论

          本文标题:最小生成树问题——Kruskal算法实现

          本文链接:https://www.haomeiwen.com/subject/tlrarqtx.html