最小生成树 java实现

最小生成树

最小生成树的定义:最小生成树是在一个给定的无向图G(V,E)中求一颗树T,使得这棵树拥有图G中所有顶点,且所有边都是来自图G中的边,并且满足整棵树的边权之和最小。

最小生成树三个性质:

  1. 最小生成树是树,因此其边数等于顶点数减一,且树内一定不会有环
  2. 对给定的图,其最小生成树可以不唯一,但边权之和一定唯一
  3. 最小生成树是在无向图上面生成的,因此其根节点可以是这颗树上面的任意一个节点。如果题目中涉及最小生成树的输出,为了让生成树唯一,一般会直接给出根节点,只需要给出节点作为根节点来求解最小生成树即可

问题种类:铺设公路

两种解决算法

  1. 普利姆算法(prim)

    1. 朴素版Prim算法(稠密图),时间复杂度O(n^2)
    2. 堆优化版Prim算法(稀疏图),时间 复杂度O(mlogn)(很少使用
  2. 克鲁斯卡尔算法(Kruskal)(稀疏图

朴素版prim算法

假如有5个节点,6条边,
A B 3
A C 1
B C 2
A E 4
C D 5
C E 6
T 表示当前包含所有节点的集合,U 表示NULL。假如从A开始,(将A加入到U中)
第一步:选中A到各个节点中权重最小的节点
第二步:判断该节点是否被访问过,如果没有被访问过,则将该节点加入到集合U中
第三步:更新其他节点到集合U的距离
第四步:选择到集合U最近的点,重复第二步

时间复杂度是 \(O(n2+m)\), n 表示点数,m 表示边数

static int n;      // n表示点数
static int INF = 0x3f3f3f3f;
static int[][] g = new int[N][N];        // 邻接矩阵,存储所有边
static int[] dist = new int[N];        // 存储其他点到当前最小生成树(集合)的距离
static boolean st = new int[N];     // 存储每个点是否已经在生成树中

// 如果图不连通,则返回INF(值是0x3f3f3f3f), 否则返回最小生成树的树边权重之和
int prim(){
      Arrays.fill(dist, 0x3f);

    //所有生成树里的边的长度之和的最小值
    int res = 0;
    for (int i = 0; i < n; i ++ ){
        int t = -1;
        for (int j = 1; j <= n; j ++ ) {
           if (st[j] != false && (t == -1 || dist[t] > dist[j])) {
               t = j;
            }
        }

        if (i != 0 && dist[t] == INF) return INF;
        if (i != 0) res += dist[t];
        st[t] = true;		
        for (int j = 1; j <= n; j ++ ) {
                    dist[j] = min(dist[j], g[t][j]);          
        }
    }

    return res;
}

对应题目

/**
 *prime算法实现
 * 输入样例:
 *      4 5
 *      1 2 1
 *      1 3 2
 *      1 4 3
 *      2 3 2
 *      3 4 4
 * 输出样例:
 *      6
 */
public class PrimAlgorithm {
    int INF = 500000;
    private Scanner sc = new Scanner(System.in);

    public void run() {
        int n = sc.nextInt();
        int m = sc.nextInt();

        int[][] graph = new int[n + 1][n + 1];
        for (int i = 0; i < n + 1; i++) {
            Arrays.fill(graph[i], INF);
        }

        for (int i = 0; i < m; i++) {
            int x = sc.nextInt();
            int y = sc.nextInt();
            int z = sc.nextInt();
            //无向图
            graph[x][y] = Math.min(graph[x][y], z);
            graph[y][x] = graph[x][y];
            graph[x][x] = 0;
            graph[y][y] = 0;
        }

        int res = prim(graph);
        if (res == INF) {
            System.out.println("impossible");
        } else {
            System.out.println(res);
        }
    }

    public int prim(int[][] graph) {
        // graph长度是n+1
        int n = graph.length - 1;
        // dist[i] 表示的是i到生成树集合的最小距离
        int[] dist = new int[n + 1];
        // 给dist初始化为无穷
        Arrays.fill(dist, INF);
        int res = 0;
        // 存储对应的边是否被访问过
        boolean[] st = new boolean[n + 1];
        // 因为之前存储graph的时候,是从下标1开始的,所以这里遍历时候就是从1开始
        for (int i = 1; i < n + 1; i++) {
            int t = -1;
            for (int j = 1; j < n + 1; j++) {
                if (st[j] == false && (t == -1 || dist[t] > dist[j])) t = j;
            }
            // 避免第一次循环的时候,直接返回
            if (i != 1 && dist[t] == INF) return INF;
            // 表示节点t已经被访问过
            st[t] = true;
            if (i != 1) {
                // 将节点添加到生成树中
                res += dist[t];
//                System.out.println(res);
            }
            // 生成树是一个集合
            // 因为生成树添加了新的节点,所以更新其他未被访问的点到生成树的最小距离,
            for (int j = 1; j < n + 1; j++) {
                dist[j] = Math.min(dist[j], graph[t][j]);
            }
        }
        return res;
    }

    public static void main(String[] args) {
        new PrimAlgorithm().run();
    }
}

Kruskal算法

参考:https://blog.csdn.net/coslay/article/details/47756917

前置知识:排序和并查集

基本思想:按照权值从小到大的顺序选择n-1条边,并保证这n-1条边不构成回路。
具体做法:首先构造一个只含n个顶点的森林,然后依权值从小到大从连通网中选择边加入到森林中,并使森林中不产生回路,直至森林变成一棵树为止。

时间复杂度是 \(O(mlogm)\), n 表示点数,m 表示边数

思路:

  1. 将所有边按权重w从小到大排序;

  2. 枚举每条边的顶点a,b

    if (a,b 不在同一个集合中(并查集)) 将这条边加入集合中

int n, m;       // n是点数,m是边数
int[] p = new int[N];       // 并查集的父节点数组

class Edge{     // 存储边
    int a, b, w;
        Edge(int a, int b, int c) {
          this.a = a;this.b = b; this.c = c;
    }
};

int find(int x){     // 并查集核心操作
    if (p[x] != x) p[x] = find(p[x]);
    return p[x];
}

public int kruskal(){
    Arrays.sort(edge, new Comparator<Edge>() {
      @Override
      public int compare(Edge o1, Edge o2) {
        if(o1.w<o2.w)   return -1;
        else if(o1.w>o2.w)  return 1;
        else return 0;
      }
    });

    for (int i = 1; i <= n; i ++ ) p[i] = i;    // 初始化并查集

    int res = 0, cnt = 0;
    for (int i = 0; i < m; i ++ ) {
        int a = edges[i].a, b = edges[i].b, w = edges[i].w;

        a = find(a);
        b = find(b);
        if (a != b) {    // 如果两个连通块不连通,则将这两个连通块合并
            p[a] = b;
            res += w;
            cnt ++ ;
        }
    }

    if (cnt < n - 1) return INF;
    return res;
}

题目:

/**
 * kruskal算法实现
 * 输入样例:
 *      4 5
 *      1 2 1
 *      1 3 2
 *      1 4 3
 *      2 3 2
 *      3 4 4
 * 输出样例:
 *      6
 */
public class KruskalAlgorithm {
    int INF = Integer.MAX_VALUE / 2;
    private Scanner sc = new Scanner(System.in);
    private int n;
    private int m;

    public static void main(String[] args) {
        new KruskalAlgorithm().run();
    }

    // 并查集
    private int find(int[] p, int x) {
        if (p[x] != x) p[x] = find(p, p[x]);
        return p[x];
    }

    private void run() {
        n = sc.nextInt();
        m = sc.nextInt();
        // 存储边
        List<Edge> edgeList = new ArrayList<>();
        // 下标从1开始,因为节点的值是从1开始的
        int[] p = new int[n + 1];
        for (int i = 1; i <= n; i++) {
            p[i] = i;
        }
      
        for (int i = 0; i < m; i++) {
            int x = sc.nextInt();
            int y = sc.nextInt();
            int z = sc.nextInt();
            edgeList.add(new Edge(x, y, z));
        }
        int res = kruskal(edgeList, p);
        if (res == INF) {
            System.out.println("impossible");
        } else {
            System.out.println(res);
        }
    }

    private int kruskal(List<Edge> edgeList, int[] p) {
        int res = 0;
        int cnt = 0;
        // 按照w 权重的大小来排序
        Collections.sort(edgeList);
        for (int i = 0; i < edgeList.size(); i++) {
            int a = edgeList.get(i).getA();
            int b = edgeList.get(i).getB();
            int w = edgeList.get(i).getW();
            // 分别找到a、b对应的集合的根
            a = find(p, a);
            b = find(p, b);
            // 如果根相同表示a、b在同一个集合中,如果添加进去,会形成回路,则该边不能添加到集合中
            // 如果根不相同表示a、b不在同一个集合中,则该边能添加到集合中
            if (a != b) {
                p[a] = b;
                res += w;
                // 计算添加进去的边
                cnt++;
            }
        }
        // n 是点的数量,n个点最少是n-1条边,如果少于n-1条边,则表示 n个点并没有全部使用到,所以不成立
        if (cnt < n - 1) return INF;
        return res;
    }
}

class Edge implements Comparable<Edge> {
    int a, b, w;
    public Edge(int a, int b, int w) {
        // a,b 为节点
        this.a = a;
        this.b = b;
        // w 为节点之间的权重
        this.w = w;
    }
    public int getA() {
        return a;
    }
    public int getB() {
        return b;
    }
    public int getW() {
        return w;
    }
    // 设置edge的排序规则,按照w的大小排序
    @Override
    public int compareTo(Edge edge) {
        return Integer.compare(w, edge.w);
    }
}
posted @ 2020-10-17 17:14  行走在代码边缘  阅读(583)  评论(0编辑  收藏  举报