求最小生成树(Prim算法与Kruskal算法与并查集)

文章详细介绍了如何使用Prim和Kruskal算法来寻找无向图的最小生成树。Prim算法通过每次添加距离最小生成树最近的边来扩展树,而Kruskal算法则按边的权重排序,避免形成回路来构建树。两种算法都涉及到并查集的使用,特别是Kruskal算法中通过并查集检测环路。文章提供了具体的Java代码实现,并附有运行结果截图。
摘要由CSDN通过智能技术生成


1、案例要求

利用贪心算法思想,求无向图的最小生成树(分别完成Prim算法、Kruskal算法,其中,Kruskal算法要求使用并查集检查回路)

假设给定的无向图如下:

在这里插入图片描述

2、算法设计与实现
2.1 Prim算法
2.1.1 构造无向图

利用二维数组构造无向图,各路径设置为双向权值相同,不通的路径权值设置为最大整数。

2.1.2 编写Prim算法函数

lowcost数组记录当前最小生成树到其余点最短距离,初始状态为其他顶点到根节点的距离,mst记录记录其余顶点到最小生成树的边所经过顶点,初始化为1,遍历根节点以外的节点,找到距离二叉树最近且未在树中的顶点,将其加入,判断其余顶点从该新顶点到二叉树的距离是否更短,更短则更新lowcost数组和mst数组,直到所有顶点被遍历完。

2.1.3 实现代码

时间复杂度:O(n2)(n为顶点数)

public static int Prim(int[][] graph, int m) {
    //设置两个数组
    // lowcost:记录当前最小生成树到其余点最短距离
    // mst:记录其余顶点到最小生成树的边所经过顶点,如mst[i]=j表示顶点i到最小生成树的边是i->j
    int[] lowcost = new int[m + 1];
    int[] mst = new int[m + 1];
    //记录最小生成树的和
    int sum = 0;

    //数组初始化
    for (int i = 2; i <= m; i++) {
        //顶点1到其余顶点距离
        lowcost[i] = graph[1][i];
        //mst初始化为1
        mst[i] = 1;
    }
    //第一个顶点已被遍历
    mst[1] = 0;

    //第二个点开始
    for (int i = 2; i <= m; i++) {
        int min = Integer.MAX_VALUE;
        int minid = 0;
        for (int j = 2; j <= m; j++) {
            //找到距离二叉树最近的顶点且未在树中
            if (lowcost[j] < min && mst[j] != 0) {
                min = lowcost[j];
                //记录顶点位置
                minid = j;
            }
        }
        //打印路径
        System.out.println(mst[minid] + "->" + minid + " " + min);
        sum += min;
        lowcost[minid] = 0;
        mst[minid] = 0;

        for (int j = 2; j <= m; j++) {
            //判断从新顶点到二叉树的距离是否更短
            if (graph[minid][j] < lowcost[j]) {
                //更新
                lowcost[j] = graph[minid][j];
                mst[j] = minid;
            }
        }
    }
    return sum;
}
2.1.4 运行结果截图

在这里插入图片描述

2.2 Kruskal算法
2.2.1 构造无向图

构造一个Edge类,有起点、终点、权值三个属性,重写compareTo函数,并用list列表存储无向图的各边。

2.2.2 编写并查集UnionFind类

union(x,y)函数实现合并功能,将节点x的父节点设置为节点y;find(x)函数实现查找功能,遍历查找x的父节点直到根节点,返回该根节点,为减少时间复杂度,同时记录查找过程的所有父节点,将所有节点的父节点设置为根节点,以便日后遍历查找耗费过多时间。

2.2.3 编写Kruskal算法

首先由边权值对边进行排序,从小到大,通过find(x)函数判断边上的两个点的根节点是否相同(判断加入后是否形成闭环),不同则加入最小生成树的边集中,同时通过union(x,y)对该边两顶点进行合并,最后直到最小生成树边集的边数==顶点数-1。

2.2.4 实现代码

时间复杂度:O(nlog2n)

并查集代码:

import java.util.HashSet;
import java.util.Set;

//并查集
public class UnionFind {
    //实现查功能
    public static UFNode find(UFNode x) {
        UFNode p = x;
        Set<UFNode> path = new HashSet<>();
        //记录向上追溯的路径上的点
        while (p.parent != null) {
            path.add(p);
            p = p.parent;
        }
        //这些点的parent全部指向这个集的代表(他们共同的老大)
        //优化:第一次未生效,后续生效,后续只需找一次就能找到当前节点的根节点
        for (UFNode ppp : path) {
            ppp.parent = p;
        }
        return p;

    }

    //实现并功能
    public static void union(UFNode x, UFNode y) {
        //将x作为y的父结点
        find(y).parent = find(x);
    }

    //定义静态内部类,这是并查集中的结点
    public static class UFNode {
        UFNode parent;//父结点
    }

}

边类代码:

public class Edge<T> implements Comparable<Edge> {
    private T start;
    private T end;
    private int distance;

    public Edge(T start, T end, int distance) {
        this.start = start;
        this.end = end;
        this.distance = distance;
    }

    public T getStart() {
        return start;
    }

    public void setStart(T start) {
        this.start = start;
    }

    public T getEnd() {
        return end;
    }

    public void setEnd(T end) {
        this.end = end;
    }

    public int getDistance() {
        return distance;
    }

    public void setDistance(int distance) {
        this.distance = distance;
    }

    @Override
    public String toString() {
        return start + "->" + end + " " + distance;
    }

    @Override
    public int compareTo(Edge o) {
        return distance > o.getDistance() ? 1 : (distance == o.getDistance() ? 0 : -1);
    }
}

Kruskal算法代码:

public class Kruskal {
    private final List<Edge> edgeList;//存放图中的所有边
    private final int n;//总顶点数

    private Set<Edge> T = new HashSet<>();//存放生成树的边
    private Map pntAndNode = new HashMap();//边上的每个顶点都和并查集中有与之对应的node

    private Set<Edge> getT() {
        buildMST();
        return T;
    }

    public Kruskal(List<Edge> edgeList, int n) {
        this.edgeList = edgeList;
        //为每个顶点建立一个并查集的点
        for (Edge edge : edgeList) {
            pntAndNode.put(edge.getStart(), new UnionFind.UFNode());
            pntAndNode.put(edge.getEnd(), new UnionFind.UFNode());
        }
        this.n = n;
    }

    //构造一个边表
    private static List<Edge> build() {
        List<Edge> li = new ArrayList<>();
        li.add(new Edge("1", "2", 2));
        li.add(new Edge("1", "6", 8));
        li.add(new Edge("1", "4", 5));
        li.add(new Edge("2", "3", 7));
        li.add(new Edge("2", "4", 7));
        li.add(new Edge("2", "5", 2));
        li.add(new Edge("3", "5", 3));
        li.add(new Edge("4", "5", 6));
        li.add(new Edge("4", "6", 7));
        li.add(new Edge("4", "7", 3));
        li.add(new Edge("5", "7", 4));
        li.add(new Edge("6", "7", 4));

        return li;
    }

    private void buildMST() {
        //先对边集进行排序
        Collections.sort(edgeList);
        for (Edge e : edgeList) {
            //寻找每条边上两个结点在map集合中映射的UFNode
            UnionFind.UFNode x = (UnionFind.UFNode) pntAndNode.get(e.getStart());
            UnionFind.UFNode y = (UnionFind.UFNode) pntAndNode.get(e.getEnd());
            if (UnionFind.find(x) == UnionFind.find(y))
                continue;//如果两个结点来自同一顶点集,则跳过这条边,否则会形成回路
            UnionFind.union(x, y);
            //把边加入到T中
            T.add(e);
            if (T.size() == n - 1)
                return;//生成树的边数==总顶点数-1,表示所有的点已经连接
        }
    }

    public static void main(String[] args) {
        List<Edge> edgeList = build();
        Kruskal obj = new Kruskal(edgeList, 7);
        //getT中就调用了buildMST方法,将生成的边放到了集合中
        for (Edge e : obj.getT())
            System.out.println(e);
    }

}
2.2.5 运行结果截图

在这里插入图片描述

3、总结

贪心算法的基本思想:每次总选择最优情况,这次的prim算法是每次总选择距离最小生成树最近的边,kruskal算法是对边集进行排序后总选择最短边。

以及复习了并查集的相关知识,其中关键的两个函数union和find分别实现的合并功能与查找功能,在查找是还可以通过相关优化算法减少时间复杂度,如父节点更新法与加权标记法,在本案例中使用了父节点更新法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值