从原理看Kruskal与Prim求最小生成树

本文详细介绍了Kruskal算法与Prim算法求最小生成树的原理与实现过程,包括贪心策略、基本定义与定理,以及算法的具体步骤与示例代码。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

从原理看Kruskal与Prim求最小生成树

最小生成树:连接图中所有节点,使各边权重之和最小的树(因为权重之和最小,所以无环)。

Kruskal算法与Prim算法都是运用的贪心算法的思想进行求解,贪心算法是每步都取最优,最后使整体最优。其中,如何判断是不是最优很关键,其次,如何证明贪心的整体最优是算法正确性的保证。接下来,我将从上述两点来剖析这两个算法。

基本定义:无向图G=(V,E)的一个切割(S,V-S)是集合V的一个划分,如果一条边(u,v)属于E,并且一个端点位于集合S,另一个端点位于V-S,则称该条边横跨切割(S,V-S)。如果集合A是边的集合,不存在横跨该切割的边,则称该切割尊重集合A。在横跨一个切割的所有边中,权重最小的变称为轻量级边

基本定理:设图G=(V,E),集合A为E的子集,且包括在图G的最小生成树中,设(S,V-S)是图G中尊重A的任意一个切割,又设(u,v)是横跨切割(S,V-S)的一条轻量级边。那么边(u,v)对于集合A是安全的(可以添加到集合A中)。

/*P364图片*/

证明:在包含A的最小生成树T中,从端点u到端点v的路径再加上边(u,v)会形成一个环。这时,我们去掉T中横跨(S,S-V)的边,再添加上(u,v),显然当前的树仍是连通的,并且因为(u,v)是轻量级边,只会小于等于去掉的边,因此得到的新树的权重和小于等于T,因此这棵新树T’也是最小生成树。因为去掉的边不属于A(因为切割尊重A,而去掉的边横跨切割),所以A属于T’,并且(u,v)也属于T’,所以(u,v)对集合A是安全的。

重要推论:(概括就是图Ga=(V,A)中,连通分量之间的轻量级边对A是安全的)设图G=(V,E),集合A为E的子集,且包括在图G的最小生成树中,设C=(Vc,Ec)是森林Ga=(V,A)中的一个连通分量。如果边(u,v)是连接C和Ga中某个其他连通分量的一条轻量级边,则(u,v)对于集合A是安全的。

证明:根据刚才的定理,我们只需证明那条轻量级边(u,v)是横跨尊重A的分割即可。因为(u,v)连接(V,A)中的两个不同的连通分量,所以按照(Vc,V-Vc)进行切割是尊重A的。为什么呢?假设不尊重A,则A中有至少一条边横跨分割,连接了这个连通分量和其它边,那么这个连通分量显然不成立了,因此按照(Vc,V-Vc)进行切割是尊重A的,所以(u,v)是横跨切割的轻量级边,由定理得推论成立。

Kruskal算法:

简要概述:每次加入到集合A中的边总是权重最小且连接两个不同连通分量的边。

基本步骤:
1.将图G=(V,E)中所有的边按照权重由小到大进行排列;
2.设A=空,按照排列顺序,对每条边进行判断,如果边(u,v)连接的是(V,A)中的两棵不同的树(即不同的连通分量),则将边(u,v)添加到A中
3.重复步骤2,直到将所有边进行遍历
4.得到的集合A即为所求最小生成树

示例代码:

import java.util.ArrayList;
import java.util.Scanner;

public class Main{
    public static void main(String[] args){
        /**
         * 输入方式:
         * 第一行输入节点数
         * 后面按照邻接矩阵的方式进行输入,因为是无向图,因此输入半个矩阵即可,对于无法连接到的节点,权重为-1
         * (节点编号从0开始)
         */
        Scanner input = new Scanner(System.in);
        int amount = input.nextInt();
        Node[] nodes = new Node[amount];
        ArrayList<Line> lines = new ArrayList<>();
        for(int i = 0; i < amount; ++i){
            nodes[i] = new Node(i,i);
        }
        for(int i = amount;i > 1; --i){
            for(int j = 0; j < i - 1; ++j){
                int weight = input.nextInt();
                if(weight != -1){
                    lines.add(new Line(amount - i,amount - i + 1 + j,weight));
                }
            }
        }
        /**
         * 按照快速排序对lines进行从小到大排序
         */
        quickSort(lines,0,lines.size() - 1);
        /**
         * 选取连接两棵树的边
         */
        ArrayList<Integer> treeNode = new ArrayList<>();
        ArrayList<Line> answer = new ArrayList<>();
        for(Line item:lines){
            if(nodes[item.getNodeNo1()].getRespresentation() !=  nodes[item.getNodeNo2()].getRespresentation()){
                int geter = nodes[item.getNodeNo1()].getRespresentation();
                int setter = nodes[item.getNodeNo2()].getRespresentation();
                for(Node node:nodes){
                    if(node.getRespresentation() == geter)
                        node.setRespresentation(setter);
                }
                answer.add(item);
            }
        }
        for(int i = 0; i < answer.size(); ++i){
            System.out.println(answer.get(i));
        }
    }

    private static int partation(ArrayList<Line> lines,int start,int end){
        int i,j;
        i = start;
        j = start - 1;
        int target = lines.get(end).getWeight();
        for(;i < end; ++i){
            if(lines.get(i).getWeight() < target){
                ++j;
                Line.swap(lines.get(i), lines.get(j));
            }
        }
        ++j;
        Line.swap(lines.get(end), lines.get(j));
        return j;
    }

    public static void quickSort(ArrayList<Line> lines,int start,int end){
        if(start < end){
            int p = partation(lines, start, end);
            quickSort(lines,p + 1,end);
            quickSort(lines,start,p - 1);
        }
    }


}
class Node{
    private int no;
    private int respresentation;

    public Node(int no,int respresentation) {
        super();
        this.no = no;
        this.respresentation = respresentation;
    }

    public int getNo() {
        return no;
    }

    public void setNo(int no) {
        this.no = no;
    }

    public int getRespresentation() {
        return respresentation;
    }

    public void setRespresentation(int respresentation) {
        this.respresentation = respresentation;
    }

}
class Line{
    private int nodeNo1;
    private int nodeNo2;
    private int weight;
    public Line(int nodeNo1, int nodeNo2, int weight) {
        super();
        this.nodeNo1 = nodeNo1;
        this.nodeNo2 = nodeNo2;
        this.weight = weight;
    }
    public Line(Line line) {
        super();
        this.nodeNo1 = line.getNodeNo1();
        this.nodeNo2 = line.getNodeNo2();
        this.weight = line.getWeight();
    }
    public int getNodeNo1() {
        return nodeNo1;
    }
    public void setNodeNo1(int nodeNo1) {
        this.nodeNo1 = nodeNo1;
    }
    public int getNodeNo2() {
        return nodeNo2;
    }
    public void setNodeNo2(int nodeNo2) {
        this.nodeNo2 = nodeNo2;
    }
    public int getWeight() {
        return weight;
    }
    public void setWeight(int weight) {
        this.weight = weight;
    }
    public void setInstance(Line line){
        this.nodeNo1 = line.getNodeNo1();
        this.nodeNo2 = line.getNodeNo2();
        this.weight = line.getWeight();
    }
    static void swap(Line a,Line b){
        Line temp = new Line(a);
        a.setInstance(b);
        b.setInstance(temp);
    }
    @Override
    public String toString() {
        return "Line [nodeNo1=" + nodeNo1 + ", nodeNo2=" + nodeNo2 + ", weight=" + weight + "]";
    }

}

示例问题:
图片

输入:
9
4 -1 -1 -1 -1 -1 8 -1
8 -1 -1 -1 -1 11 -1
7 -1 4 -1 -1 2
9 14 -1 -1 -1
10 -1 -1 -1
2 -1 -1
1 6
7
输出:
Line [nodeNo1=6, nodeNo2=7, weight=1]
Line [nodeNo1=5, nodeNo2=6, weight=2]
Line [nodeNo1=2, nodeNo2=8, weight=2]
Line [nodeNo1=0, nodeNo2=1, weight=4]
Line [nodeNo1=2, nodeNo2=5, weight=4]
Line [nodeNo1=2, nodeNo2=3, weight=7]
Line [nodeNo1=1, nodeNo2=2, weight=8]
Line [nodeNo1=3, nodeNo2=4, weight=9]

Prim算法:

简要概述:每次加入到集合A中的边总是邻接集合A且权重最小、连接两个不同连通分量的边(即将Kruskal中的选择范围缩小到了集合A的邻接边)。

基本步骤:
1.将选取的根节点设置距离(key)为0,其他节点距离(key)为无穷;
2.将所有节点添加到队列Q中;
3.弹出Q中距离最小的节点u,遍历与u相连的节点v,对于属于Q并且(u,v)权重小于v.key的节点v,设置v.key=(u,v)的权重;
4.重复步骤3直到Q为空。

import java.util.ArrayList;
import java.util.PriorityQueue;
import java.util.Scanner;

public class Main {
    public static void main(String[] args){
        /**
         * 输入方式:
         * 先输入节点的数量
         * 后面输入图的邻接矩阵
         * 最后输入指定的根节点的No
         */
        Scanner input = new Scanner(System.in);
        int amount = input.nextInt();
        Node[] nodes = new Node[amount];
        int[][] weight = new int[amount][amount];
        for(int i = 0; i < amount; ++i){
            for(int j = 0; j < amount; ++j){
                weight[i][j] = input.nextInt();
            }
            nodes[i] = new Node(i);
        }
        int rootNo = input.nextInt();
        /**
         * 将根节点的key设置为0,这样第一次循环先从根节点开始
         */
        nodes[rootNo].setKey(0);
        /**
         * 设置一个最小优先队列Q,将所有当前结点添加到Q中
         * 设置一个集合A来保存已经添加的节点
         */
        PriorityQueue<Node> Q = new PriorityQueue<>();
        for(int i = 0; i < amount; ++i)
            Q.add(nodes[i]);
        ArrayList<Node> A = new ArrayList<>();
        /**
         * 开始进行循环
         * 每次在Q中挑选key最小的节点,添加到A中,修改Q的邻接节点的key
         * 直到Q中不再含有节点
         */

        while(!Q.isEmpty()){
            Node u = Q.poll();
            /**
             * 刷新优先队列的排序
             */
            Q.add(u);
            u = Q.poll();


            A.add(u);
            int no = u.getNo();
            for(int i = 0; i < weight[no].length; ++i){
                if(weight[no][i] != -1 && i != no && Q.contains(nodes[i]) && weight[no][i] < nodes[i].getKey()){
                    nodes[i].setKey(weight[no][i]);
                    nodes[i].setParent(u);
                }
            }
        }
        /**
         * 输出结果
         */
        for(int i = 0; i < A.size(); ++i){
            System.out.println(A.get(i));
        }

    }
}

class Node implements Comparable<Node>{
    private int no;
    private Node parent = null;
    private int key = 1000000000;
    public int getNo() {
        return no;
    }
    public void setNo(int no) {
        this.no = no;
    }
    public int getKey() {
        return key;
    }
    public void setKey(int key) {
        this.key = key;
    }
    public Node getParent() {
        return parent;
    }
    public void setParent(Node parent) {
        this.parent = parent;
    }
    public Node(int no) {
        super();
        this.no = no;
    }
    @Override
    public String toString() {
        return "Node [no=" + no + ", parent=" + (parent == null?null:parent.getNo()) + ", key=" + key + "]";
    }
    @Override
    public int compareTo(Node o) {
        return key - o.getKey();
    }

}

Prim算法有一个bug,就是刷新优先队列时得不到最小的值…博主找了一天也没找到问题,有发现原因的朋友请在底下留言,跪谢,呜呜呜…

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值