图论与算法(6)最小生成树

news2024/11/22 14:16:00

1. 带权图及实现

1.1 带全图概述

带权图是一种图形结构,其中图中的边具有权重或成本。每条边连接两个顶点,并且具有一个与之关联的权重值,表示了两个顶点之间的某种度量、距离或成本。

带权图可以用邻接矩阵或邻接表来表示。邻接矩阵是一个二维矩阵,其中行和列表示图中的顶点,矩阵中的元素表示边的权重。邻接矩阵可以是一个二维数组,也可以是一个字典等其他数据结构。邻接表是一种以顶点为键的字典或映射数据结构,每个顶点关联一个边列表或链表,边列表中存储与该顶点相邻的顶点及其权重。

1.2 实现带权图

package WeightedGraph;

import java.io.File;
import java.io.FileNotFoundException;
import java.util.*;

/**
 * 权重图类
 * @author wushaopei
 */
public class WeightedGraph {

    private TreeMap<Integer, Integer>[] adj; // 邻接表数组
    private int V,E;   // 邻接表数组

    /**
     * 构造函数,根据文件名读取图的信息并创建权重图对象
     * @param fileName 文件名
     */
    public WeightedGraph(String fileName){

        File file = new File(fileName);

        try (Scanner scanner = new Scanner(file)){
            V = scanner.nextInt();   // 读取顶点数
            if (V < 0) throw new IllegalArgumentException("V must be non-negative");

            E = scanner.nextInt();  // 读取边数
            if (E < 0 ) throw new IllegalArgumentException("V must be non-negative");

            adj = new TreeMap[V]; // 初始化邻接表数组

            for (int i = 0; i < V; i++){
                adj[i] = new TreeMap<>();  // 每个顶点关联一个邻接表
            }

            for (int j = 0; j < E; j ++){

                int a = scanner.nextInt();  // 边的一个顶点
                int b = scanner.nextInt();  // 边的一个顶点

                int weight = scanner.nextInt(); // 边的一个顶点

                if (a == b) throw new IllegalArgumentException("Self Loop is Detected!");  // 检测自环边
                // 检测平行边
                if (adj[a].containsKey(b)) throw new IllegalArgumentException("Parallel Edges are Detected!");

                adj[a].put(b,weight);  // 在邻接表中添加边及其权重
                adj[b].put(a,weight);
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
    }

    /**
     * 验证顶点的有效性
     * @param v 顶点
     */
    public void validateVertex(int v){
        if ( v < 0 && v > V){
            throw new IllegalArgumentException("vertex " + v + " is invalid.");
        }
    }

    /**
     * 获取图的顶点数
     * @return 顶点数
     */
    public int V(){
        return V;
    }

    /**
     * 获取图的边数
     * @return 边数
     */
    private int E(){
        return E;
    }

    /**
     * 判断两个顶点之间是否存在边
     * @param v 顶点v
     * @param w 顶点w
     * @return 是否存在边
     */
    public boolean hashEdge(int v, int w){
        validateVertex(v);
        validateVertex(w);
        return adj[v].containsKey(w);
    }

    /**
     * 获取顶点的邻接链表
     * @param v 顶点
     * @return 邻接链表
     */
    public Iterable<Integer> adj(int v){
        validateVertex(v);
        return adj[v].keySet();
    }

    /**
     * 获取两个顶点之间的权重
     * @param v 顶点v
     * @param w
     */
    public int getWeighted(int v , int w){
        if (hashEdge(v,w))
            return adj[v].get(w);
        throw new IllegalArgumentException(String.format("No edge %s-%s.", v, w));
    }

    /**
     * 获取顶点的度数
     * @param v 顶点
     * @return 顶点的度数
     */
    public int degree(int v){
        validateVertex(v);
        return adj[v].size();
    }

    /**
     * 删除两个顶点之间的边
     * @param v 顶点v
     * @param w 顶点w
     */
    public void removeEdge(int v, int w){
        validateVertex(v);
        validateVertex(w);
        adj[v].remove(w);
        adj[w].remove(v);
    }

    public static void main(String[] args) {
        WeightedGraph weightedGraph = new WeightedGraph("src/weight.txt");
        System.out.println(weightedGraph.toString());
    }
}

在带权图的实现中,常用的数据结构是邻接表(adjacency list)。邻接表是一个数组,数组的每个元素对应图中的一个顶点,每个顶点对应一个链表,链表中存储与该顶点相邻的顶点及其对应的权重。

具体实现中,可以使用以下数据结构来表示带权图:

  1. 顶点数和边数:使用变量 V 表示顶点数,使用变量 E 表示边数。

  2. 邻接表数组:使用一个数组 adj 来存储邻接表。adj 的长度为顶点数 V,每个元素是一个 TreeMap<Integer, Integer> 对象,用于存储与该顶点相邻的顶点和对应的权重。其中,TreeMap 是按照键的顺序进行排序的有序映射。

  3. 构造函数:构造函数用于从文件中读取图的信息并初始化邻接表。在构造函数中,读取文件的过程中可以完成以下操作:

    • 读取顶点数和边数,并进行合法性检查。
    • 初始化邻接表数组 adj,为每个顶点创建一个空的 TreeMap 对象。
    • 读取每条边的起点、终点和权重,并将其添加到对应顶点的邻接表中。

通过使用邻接表来表示带权图,可以高效地进行图的遍历和相关操作。同时,邻接表的存储结构也适用于稀疏图,节省了存储空间。

2. Map 的遍历

@Override
    protected Object clone() {

        try {
            WeightedGraph cloned = (WeightedGraph) super.clone();
            cloned.adj = new TreeMap[V];
            for (int v = 0; v < V; v ++){
                cloned.adj[v] = new TreeMap<Integer,Integer>();
                for (Map.Entry<Integer,Integer> entry: adj[v].entrySet()){
                    cloned.adj[v].put(entry.getKey(),entry.getValue());
                }
            }
            return cloned;
        }catch (CloneNotSupportedException e){
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 重写toString方法,打印图的信息
     * @return 图的信息字符串
     */
    @Override
    public String toString() {
        StringBuffer stringBuffer = new StringBuffer();
        stringBuffer.append(String.format("V = %d, E = %d\n",V,E));
        for (int i = 0 ; i < V; i ++){
            stringBuffer.append(String.format("%d : ",i));
            for (Map.Entry<Integer,Integer> entry: adj[i].entrySet()){
                stringBuffer.append(String.format("(%d: %d)", entry.getKey(), entry.getValue()));
            }
            stringBuffer.append("\n");
        }
        return stringBuffer.toString();
    }

测试带权图生成邻接表代码:

    public static void main(String[] args) {
        WeightedGraph weightedGraph = new WeightedGraph("src/weight.txt");
        System.out.println(weightedGraph.toString());
    }

数:

7 12
0 1 2
0 3 7
0 5 2
1 2 1
1 3 4
1 4 3
1 5 5
2 4 4
2 5 4
3 4 1
3 6 5
4 6 7

生成的邻接表:

V = 7, E = 12
0 : (1: 2)(3: 7)(5: 2)
1 : (0: 2)(2: 1)(3: 4)(4: 3)(5: 5)
2 : (1: 1)(4: 4)(5: 4)
3 : (0: 7)(1: 4)(4: 1)(6: 5)
4 : (1: 3)(2: 4)(3: 1)(6: 7)
5 : (0: 2)(1: 5)(2: 4)
6 : (3: 5)(4: 7)


Process finished with exit code 0

3. 最小生成树和 Kruskal 算法

 3.1 生成树

生成树(Spanning Tree)是指在一个无向连通图中,选择部分边和顶点,构成一棵树,使得这棵树包含了图中的所有顶点,且不包含任何回路(环)。

生成树有以下特点:

  • 生成树的边数比顶点数少一。
  • 生成树中的边必须保证连接图中的所有顶点。
  • 生成树不能包含回路。

上图是一个无向有权图,根据图可以知道,它有以下两种生成树:

3.2 最小生成树

最小生成树(Minimum Spanning Tree,简称 MST)是指在一个带权连通图中,找到一棵包含所有顶点的树,使得树的边权重之和最小。

常用的最小生成树算法有以下两种:

3.3 Prim算法:

  • 从图中任意选择一个顶点作为起始点,将其加入最小生成树中。
  • 以起始点为基础,不断选择与当前最小生成树相连的权重最小的边,并将其连接的顶点加入最小生成树中,直到最小生成树包含了图中的所有顶点。
  • Prim算法可以使用优先队列(最小堆)来维护当前最小生成树和未加入最小生成树的顶点之间的边的权重,以便快速选择最小的边。

3.4 Kruskal算法:

  • 将图中的所有边按照权重从小到大排序。
  • 依次选择权重最小的边,若该边的两个顶点不在同一个连通分量中,则将该边加入最小生成树中,并将两个顶点合并到同一个连通分量中。
  • 重复上述步骤,直到最小生成树包含了图中的所有顶点。
  • Kruskal算法可以使用并查集来快速判断两个顶点是否在同一个连通分量中,以及合并两个连通分量。

这两种算法都能得到最小生成树,但在不同的应用场景下可能有不同的性能表现。Prim算法适用于稠密图,而Kruskal算法适用于稀疏图。选择哪种算法取决于图的规模、稀疏程度以及算法的实现方式。

最小生成树算法在实际应用中有着广泛的应用,例如网络设计、电力传输、城市规划等领域。它可以帮助找到最优的连接方式或路径,以实现资源的最优利用和成本的最小化。

4.切分定理

4.1 切分定理

切分定理(Cut Property)是图论中的一个基本定理,它描述了生成树和图的边的关系。

切分定理的表述如下:

对于一个连通图,任意选择一个切分(Cut),即将图的顶点集合分为两个非空的子集,那么切分的边中权重最小的边必然属于该图的最小生成树。

换句话说,对于一个连通图,任意选择一个切分,最小生成树中的边必然包含了切分中的权重最小的边。

切分定理的证明可以通过反证法进行推导。假设最小生成树中不包含切分中的权重最小的边,那么可以通过添加该边来构造出一个权重更小的生成树,与最小生成树的定义相矛盾,因此切分定理成立。

切分定理在最小生成树算法中起到重要的作用。基于切分定理,一些最小生成树算法(如Prim算法)可以通过不断地选择切分中的最小权重边来构建最小生成树,从而提高算法的效率。切分定理也为理解生成树和图的关系提供了重要的理论基础。

4.2 切分与横切变

在图论中,切分(Cut)和横切边(Crossing Edge)是相关的概念,它们描述了图中的边如何被分割和跨越切分的情况。

切分(Cut)是将图的顶点集合分为两个非空的子集的操作。一个切分将图的顶点集合分为两个部分,称为切分的两侧。如果顶点集合被划分为A和B两个子集,那么切分就可以表示为(A, B)。注意,切分只关注顶点集合的划分,并不关注具体的边。

横切边(Crossing Edge)是指连接切分的两侧的边,即一个顶点在切分的一侧,另一个顶点在切分的另一侧的边。换句话说,横切边横跨了切分的边界,连接了不同的子集。

切分与横切边在最小生成树算法中有重要的应用。切分定理指出,对于一个连通图的切分,最小生成树中的边必然包含切分中的权重最小的边。因此,在最小生成树算法(如Prim算法和Kruskal算法)中,通过选择切分中的最小权重边,可以逐步构建最小生成树。这样,横切边就是在切分过程中被添加到最小生成树中的边,它们连接了不同的子集。

总结起来,切分是将图的顶点集合划分为两个子集的操作,而横切边是连接切分的两侧的边。切分定理指出最小生成树中的边必然包含切分中的权重最小的边,因此在最小生成树算法中,通过选择切分中的最小权重边,可以逐步构建最小生成树,并将横切边添加到最小生成树中。

5. Kruskal 算法的实现

5.1 Kruskal算法

Kruskal算法是一种用于构建最小生成树的贪心算法。它的基本思想是从图中的边集合中逐步选择权重最小的边,并且确保所选择的边不会形成环路,直到最小生成树形成为止。

以下是Kruskal算法的步骤简述:

  1. 创建一个空的边集合,用于存储最小生成树的边。
  2. 对图中的所有边按照权重进行排序。
  3. 遍历排序后的边集合,依次考虑每条边:
    • 如果当前边的加入不会导致形成环路,则将该边加入最小生成树的边集合中。
    • 否则,跳过该边。
  4. 当最小生成树的边数达到图中顶点数减一时,算法终止。
  5. 返回最小生成树的边集合作为结果。

简而言之,Kruskal算法通过不断选择权重最小的边,并确保所选择的边不会形成环路,来构建最小生成树。它是一种贪心算法,每次选择当前最优的边,最终得到的最小生成树具有最小的总权重。

5.2 步骤1、2的实现

public class Kruskal {

    private WeightedGraph G;  // 带权图对象
    private List<WeightedEdge> edges;  // 存储边的列表

    public Kruskal(WeightedGraph G){
        this.G = G;

        CC cc = new CC(G);  // 创建连通分量对象
        if (cc.count() > 1){  // 如果图的连通分量数量大于1,即图不连通
            throw new IllegalArgumentException("The graph is not connected.");
        }

        edges = new ArrayList<>();  // 初始化边的列表

        // 遍历图的所有顶点
        for (int v = 0; v < G.V(); v ++){
            for (Integer w : G.adj(v)) {  // 遍历顶点的邻接顶点
                if (v < w)
                    edges.add(new WeightedEdge(v,w,G.getWeighted(v,w)));  // 将边加入列表中
            }
        }
        Collections.sort(edges);  // 对边的列表按权重进行排序
    }
}

5.3 权重图类

/**
 * 权重图类
 * @author wushaopei
 */
public class WeightedEdge implements Comparable<WeightedEdge> {

   private int v,w,weight;

   public WeightedEdge(int v, int w, int weight){
       this.v = v;
       this.w = w;
       this.weight = weight;
   }

    @Override
    public String toString() {
        return String.format("%d:%d-%d", v,w,weight);
    }

    @Override
    public int compareTo(WeightedEdge another) {
        return weight - another.weight;
    }
}

创建一个空的边集合,用于存储最小生成树的边。

6. 并查集动态环检测

6.1 并查集概述

并查集(Disjoint Set)是一种数据结构,用于处理集合的合并和查询操作。它主要用于解决一些与集合划分相关的问题,例如连通性判断、最小生成树的构建等。

在并查集中,每个元素都被看作是一个节点,并按照一定规则组织在集合中。每个节点都有一个指向父节点的指针,初始状态下,每个节点都是一个独立的集合,其父节点指向自身。

并查集主要包含以下几个操作:

  1. 初始化:创建一个并查集对象,指定元素的个数。初始状态下,每个元素都是一个独立的集合,其父节点指向自身。

  2. 查找根节点:通过递归查找的方式,找到某个节点所在集合的根节点。在查找过程中,将经过的所有节点的父节点指向根节点,以加速后续的查找操作,这一过程被称为路径压缩。

  3. 判断连通性:通过查找元素所在集合的根节点来判断两个元素是否属于同一个集合,即是否连通。

  4. 合并集合:将两个集合合并为一个集合,具体操作是将其中一个集合的根节点的父节点指向另一个集合的根节点。

在具体应用中,可以通过并查集实现一些常见的功能,例如判断图中的两个顶点是否连通、构建最小生成树等。在构建最小生成树时,可以按照边的权重从小到大进行排序,然后依次加入边,如果加入的边的两个顶点不属于同一个集合(即不连通),则将它们合并,并将边加入最小生成树的边集合中。

这种基于并查集的思想可以有效地处理图的连通性问题,具有较高的效率。通过路径压缩和按秩合并等优化策略,可以进一步提高并查集的性能。

6.2 并查集实现

package WeightedGraph;

/**
 * @author wushaopei
 */
public class UF{

    private int[] parent;

    public UF(int n){

        parent = new int[n];
        for(int i = 0 ; i < n ; i ++)
            parent[i] = i;
    }

    public int find(int p){
        if( p != parent[p] )
            parent[p] = find( parent[p] );
        return parent[p];
    }

    public boolean isConnected(int p , int q){
        System.out.println(find(p) +"-"+ find(q));
        return find(p) == find(q);
    }

    public void unionElements(int p, int q){

        int pRoot = find(p);
        int qRoot = find(q);

        if( pRoot == qRoot )
            return;

        parent[pRoot] = qRoot;
    }
}

在构造并查集对象时,通过循环将每个顶点的父节点初始化为自身,即 parent[i] = i,这样每个顶点就形成了独立的集合。

当调用 find(p) 方法时,如果 p 不是根节点(即 p != parent[p]),则通过递归调用 find( parent[p] ) 来查找 p 的根节点。在这个过程中,会沿着顶点的父节点一直向上查找,直到找到根节点。最后,将经过的所有顶点的父节点都指向根节点,从而实现路径压缩,加快后续查找的速度。

因此,当执行 find(p)find(q) 时,会分别返回 pq 的根节点。如果它们的根节点不相同,即 find(p) != find(q),则说明 pq 不属于同一个连通分量,也就是不联通。

当调用 unionElements(p, q) 方法时,会先通过 find(p)find(q) 分别找到 pq 的根节点,然后将其中一个根节点的父节点指向另一个根节点,从而将两个集合合并为一个集合。

这样,在使用并查集判断两个顶点是否连通时,通过比较它们的根节点是否相同,即 find(p) == find(q),来判断它们是否属于同一个连通分量。如果它们的根节点相同,说明它们是连通的;如果根节点不同,说明它们不连通。

6.3 并查集实现最小生成树

package WeightedGraph;

import java.util.*;

/**
 * @author wushaopei
 * @create 2023-06-06 13:41
 */
public class Kruskal {

    private WeightedGraph G;  // 带权图对象
    private ArrayList<WeightedEdge> edges;  // 存储边的列表
    private ArrayList<WeightedEdge> mst;

    public Kruskal(WeightedGraph G){
        this.G = G;

        CC cc = new CC(G);  // 创建连通分量对象
        if (cc.count() > 1){  // 如果图的连通分量数量大于1,即图不连通
            throw new IllegalArgumentException("The graph is not connected.");
        }

        edges = new ArrayList<>();  // 初始化边的列表

        // 遍历图的所有顶点
        for (int v = 0; v < G.V(); v ++){
            for (Integer w : G.adj(v)) {  // 遍历顶点的邻接顶点
                if (v < w)
                    edges.add(new WeightedEdge(v,w,G.getWeighted(v,w)));  // 将边加入列表中
            }
        }

        Collections.sort(edges);  // 对边的列表按权重进行排序

        UF uf = new UF(G.V());
        mst = new ArrayList<WeightedEdge>();
        for (WeightedEdge edge : edges) {
            int v = edge.getV();
            int w = edge.getW();
            if (!uf.isConnected(v,w)){
                mst.add(edge);
                uf.unionElements(v,w);
            }
        }
    }

    public ArrayList<WeightedEdge> result(){
        return mst;
    }

    public static void main(String[] args) {
        WeightedGraph weightedGraph = new WeightedGraph("src/weight.txt");
        Kruskal kruskal = new Kruskal(weightedGraph);
        System.out.println(kruskal.result());
    }
}

执行结果:

[1-2: 1, 3-4: 1, 0-1: 2, 0-5: 2, 1-4: 3, 3-6: 5]

时间复杂度: O(ElogE)

6.4 Kruskal算法实现步骤说明

Kruskal算法是一种常用于解决最小生成树问题的贪心算法。它通过逐步选择图中权重最小的边,并保证边的选择不会形成环路,从而构建最小生成树。

以下是Kruskal算法的实现步骤:

  1. 创建一个空的边列表edges,用于存储图中的所有边。
  2. 对图G进行连通性检查,可以使用并查集或深度优先搜索等方法。如果图G不是连通图(即有多个连通分量),则无法构建最小生成树。
  3. 遍历图G的所有顶点v:
    • 对于顶点v,遍历其邻接顶点w,并将边(v, w)加入edges列表。注意,由于图是无向图,需要保证只加入一次。
  4. 对edges列表按照边的权重进行升序排序。
  5. 创建一个空的最小生成树边列表mst,用于存储最小生成树的边。
  6. 初始化一个并查集uf,大小为图G的顶点数。
  7. 遍历edges列表中的每一条边edge:
    • 获取边edge的起点v和终点w。
    • 如果uf中v和w不连通(即find(v) != find(w)),则将边edge加入mst列表,并调用uf.unionElements(v, w)将v和w合并到同一个集合中。
  8. 完成遍历后,mst列表中存储的边即为最小生成树的边集合。

Kruskal算法的核心思想是根据边的权重逐步选择边,并确保所选的边不会形成环路。通过并查集的帮助,可以高效地判断两个顶点是否连通,并避免选择形成环路的边。

7. Prim 算法的原理及模拟

7.1 概述

Prim算法是一种常用于解决最小生成树问题的贪心算法。它从一个起始顶点开始,逐步扩展最小生成树的边集合,直到覆盖所有顶点。

以下是Prim算法的实现步骤:

  1. 创建一个空的最小生成树边列表mst,用于存储最小生成树的边。
  2. 选择一个起始顶点作为树的根节点,将其加入最小生成树。
  3. 创建一个空的边列表edges,用于存储候选边。
  4. 将起始顶点的所有邻接边加入edges列表。
  5. 重复以下步骤,直到mst包含所有顶点:
    • 从edges列表中选择一条权重最小的边edge。
    • 如果边edge的终点不在mst中,将边edge加入mst列表,并将边edge的终点加入mst。
    • 将边edge的终点的所有邻接边加入edges列表。
    • 从edges列表中删除边edge。
  6. 完成循环后,mst列表中存储的边即为最小生成树的边集合。

Prim算法的核心思想是通过选择权重最小的边来逐步扩展最小生成树,保证每一步加入的边都是连接已经覆盖的顶点和未覆盖的顶点的最短边。通过不断更新候选边列表edges和最小生成树边列表mst,最终得到最小生成树。

需要注意的是,Prim算法可以使用不同的数据结构来实现,如优先队列(最小堆)、堆、红黑树等,以便高效地选择权重最小的边。具体的实现方式可能因编程语言和数据结构的选择而有所差异,但基本思想是相同的。

7.2 模拟

操作切分,从1:V-1 开始,每次找当前切分的最短横切边;扩展切分,直到没有切分。

8. 实现 Prim 算法

package WeightedGraph;

import java.util.ArrayList;
import java.util.Collections;

/**
 * @author wushaopei
 */
public class Prim {

    private WeightedGraph G;  // 带权图对象
    private ArrayList<WeightedEdge> edges;  // 存储边的列表
    private ArrayList<WeightedEdge> mst;

    public Prim(WeightedGraph G){
        this.G = G;
        mst = new ArrayList<WeightedEdge>();

        CC cc = new CC(G);  // 创建连通分量对象
        if (cc.count() > 1){  // 如果图的连通分量数量大于1,即图不连通
            throw new IllegalArgumentException("The graph is not connected.");
        }

        edges = new ArrayList<>();  // 初始化边的列表

        // 遍历图的所有顶点
        for (int v = 0; v < G.V(); v ++){
            for (Integer w : G.adj(v)) {  // 遍历顶点的邻接顶点
                if (v < w)
                    edges.add(new WeightedEdge(v,w,G.getWeighted(v,w)));  // 将边加入列表中
            }
        }

        Collections.sort(edges);  // 对边的列表按权重进行排序

        UF uf = new UF(G.V());
        boolean[] visited = new boolean[G.V()];
        visited[0] = true;
        // 顶点数
        for (int i = 0 ; i < G.V(); i ++){
            WeightedEdge minWeightedEdge = new WeightedEdge(-1,-1, Integer.MAX_VALUE);
            // 扫描所有边
            for ( int v = 0; v < G.V(); v ++){
                if (visited[v]){
                    for (Integer w : G.adj(v)) {
                        if ( !visited[w] && G.getWeighted(v,w) < minWeightedEdge.getWeight()){
                            minWeightedEdge = new WeightedEdge(v,w,G.getWeighted(v,w));
                        }
                    }
                }
            }
            if (minWeightedEdge.getWeight() != Integer.MAX_VALUE){
                mst.add(minWeightedEdge);
                visited[minWeightedEdge.getV()] = true;
                visited[minWeightedEdge.getW()] = true;
            }
        }
    }

    public ArrayList<WeightedEdge> result(){
        return mst;
    }

    public static void main(String[] args) {
        WeightedGraph weightedGraph = new WeightedGraph("src/weight.txt");
        Prim kruskal = new Prim(weightedGraph);
        System.out.println(kruskal.result());
    }
}

时间复杂度:O((V-1) * (V+E)) = O(VE)

9. Prim 算法的优化

9.1 基于优先队列优化的思路

(1)初始化一个空的最小生成树边列表 `mst`。

(2)创建连通分量对象 `cc`,并检查图是否连通。如果图的连通分量数量大于1,则抛出异常,表示图不连通。

(3)初始化边的列表 `edges`。

(4)遍历图的所有顶点,对于每个顶点 `v`,遍历其邻接顶点 `w`:

   - 如果顶点 `v` 的索引小于顶点 `w` 的索引,则将边 `(v, w)` 加入边列表 `edges`。

(5) 对边列表 `edges` 按权重进行排序。

(6)创建并查集对象 `uf`。

(7)创建布尔数组 `visited`,用于记录顶点是否被访问。将起始顶点(例如索引为 0 的顶点)标记为已访问。

(8)创建优先队列 `pq`(优先级队列),用于存储边的权值。

(9)遍历起始顶点的邻接顶点,将与起始顶点相连的边 `(0, w)` 加入优先队列 `pq`。

(10)循环直到优先队列 `pq` 为空:

    - 从优先队列 `pq` 中取出权值最小的边 `minEdge`。

    - 如果边 `minEdge` 的两个顶点都已经被访问过,则跳过当前循环。

    - 将边 `minEdge` 加入最小生成树的边列表 `mst`。

    - 获取边 `minEdge` 的未访问顶点 `newv`(如果 `minEdge` 的顶点 `v` 已访问,则取顶点 `w`;如果顶点 `w` 已访问,则取顶点 `v`)。

    - 将顶点 `newv` 标记为已访问。

    - 遍历顶点 `newv` 的邻接顶点 `w`,如果顶点 `w` 未访问,则将边 `(newv, w)` 加入优先队列 `pq`。

(11)返回最小生成树的边列表 `mst`。

该算法利用了优先队列的特性,每次选择权值最小的边来扩展最小生成树的边集合,直到所有顶点都被访问过。这种优化减少了不必要的遍历和比较操作,提高了算法的效率。

9.2 基于优先队列优化Prim算法:

 public PrimQueue(WeightedGraph G){
        this.G = G;
        mst = new ArrayList<WeightedEdge>();

        CC cc = new CC(G);  // 创建连通分量对象
        if (cc.count() > 1){  // 如果图的连通分量数量大于1,即图不连通
            throw new IllegalArgumentException("The graph is not connected.");
        }

        edges = new ArrayList<>();  // 初始化边的列表

        // 遍历图的所有顶点
        for (int v = 0; v < G.V(); v ++){
            for (Integer w : G.adj(v)) {  // 遍历顶点的邻接顶点
                if (v < w)
                    edges.add(new WeightedEdge(v,w,G.getWeighted(v,w)));  // 将边加入列表中
            }
        }

        Collections.sort(edges);  // 对边的列表按权重进行排序

        UF uf = new UF(G.V());
        boolean[] visited = new boolean[G.V()];
        visited[0] = true;
        // 顶点数
        Queue pq = new PriorityQueue<WeightedEdge>();
        for (int w:G.adj(0)){
            pq.add(new WeightedEdge(0,w, G.getWeighted(0,w)));
        }
        while (!pq.isEmpty()){
            WeightedEdge minEdge = (WeightedEdge)pq.remove();
            if (visited[minEdge.getV()] && visited[minEdge.getW()]){
                continue;
            }
            mst.add(minEdge);

            int newv = visited[minEdge.getV()]?minEdge.getW():minEdge.getV();
            visited[newv] = true;

            for (int w : G.adj(newv)) {
                if (!visited[w]){
                    pq.add(new WeightedEdge(newv,w,G.getWeighted(newv,w)));
                }
            }
        }

    }

时间复杂度:O(ElogE)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/618511.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

集成电路(芯片)中VCC、VDD、VSS、GND和AGND等概念

IC芯片 Integrated Circuit Chip 即集成电路芯片&#xff0c;是将大量的微电子元器件(晶体管、电阻、电容、二极管等) 形成的集成电路放在一块塑基上&#xff0c;做成一块芯片。目前几乎所有看到的芯片&#xff0c;都可以叫做 IC芯片 。 SOP与DIP SOP(Small Outline Package…

浅谈备考 系统架构师

这里写自定义目录标题 准备步骤考试形式考试内容学习考试内容训练考试内容其他觉得好的同类参考资料2023年度计算机技术与软件专业技术资格&#xff08;水平&#xff09;考试工作计划 第一次产生萌芽的时候三年前&#xff0c;当初备考没有想过要评职称或者成为什么人才&#xf…

antd3和dva-自定义组件初始化值的操作演示和自定义组件校验

前言 在antd3 (react)版和dva下,好像有的项目使用的是getFieldDecorator来获取表单的值的,现在就遇到了一个问题,getFieldDecorator针对antd自带的组件实现效果很好,除去一个form.item只能有一个getFieldDecorator的限制,其他都很好用,但是假如是自定义组件或者说在getFieldDec…

Linux内存管理7——深入理解 slab cache 内存分配全链路实现

1. slab cache 如何分配内存 当我们使用 fork() 系统调用创建进程的时候&#xff0c;内核需要为进程创建 task_struct 结构&#xff0c;struct task_struct 是内核中的核心数据结构&#xff0c;当然也会有专属的 slab cache 来进行管理&#xff0c;task_struct 专属的 slab cac…

iperf3使用

目录 写在前面&#xff1a;带宽和吞吐量安装使用测试TCP吞吐量测试UDP吞吐量测试上下行带宽&#xff08;TCP双向传输&#xff09;测试多线程TCP吞吐量测试上下行带宽&#xff08;UDP双向传输&#xff09;测试多线程UDP吞吐量 iperf3常用参数通用参数server端参数client端参数 i…

一种星载系统软件定义平台的设计与实现.v3

摘要 针对星载综合射频开放式系统架构&#xff0c;为了在软件综合层面上实现波形应用软件与具体平台的解耦&#xff0c;设计并实现了一种基于软件通信架构&#xff08;Software Communication Architecture, SCA&#xff09;的软件平台及其环境工具。通过解决星载平台软件的分…

linuxOPS基础_linux自有服务systemctl

自有服务概述 ​ 服务是一些特定的进程&#xff0c;自有服务就是系统开机后就自动运行的一些进程&#xff0c;一旦客户发出请求&#xff0c;这些进程就自动为他们提供服务&#xff0c;windows系统中&#xff0c;把这些自动运行的进程&#xff0c;称为"服务" ​ 举例…

总结888

学习目标&#xff1a; 月目标&#xff1a;6月&#xff08;线性代数强化9讲2遍&#xff0c;背诵15篇短文&#xff0c;考研核心词过三遍&#xff09; 周目标&#xff1a;线性代数强化1讲&#xff0c;英语背3篇文章并回诵&#xff0c;检测 每日必复习&#xff08;5分钟&#xff…

Java 基础第八章: 接口、内部类、包装类

参考资料 &#xff1a;康师傅的视频课 方法 、 有继承的代码块的加载顺序&#xff1a;先执行父类的静态代码块、子类的静态代码块&#xff1b;然后&#xff0c;执行父类的普通代码块和构造器 子类的的普通代码块和构造器&#xff1b; 总结&#xff1a;由父到子&#xff0c;静…

【Web服务器】Nginx之Rewrite与location的用法

文章目录 前言一、正则表达式1. Nginx 的正则表达式2. 正则表达的优势3. Nginx 使用正则的作用 二、location 的概念1. location 和 rewrite 区别2. location 匹配的分类3. location 常用的匹配规则3.1 location 匹配优先级3.2 location 匹配的实例3.3 实际网站规则定义第一个必…

深度学习应用篇-计算机视觉-图像分类[2]:LeNet、AlexNet、VGG、GoogleNet、DarkNet模型结构、实现、模型特点详细介绍

【深度学习入门到进阶】必看系列&#xff0c;含激活函数、优化策略、损失函数、模型调优、归一化算法、卷积模型、序列模型、预训练模型、对抗神经网络等 专栏详细介绍&#xff1a;【深度学习入门到进阶】必看系列&#xff0c;含激活函数、优化策略、损失函数、模型调优、归一化…

RabbitMQ - 发布确认

RabbitMQ - 发布确认 发布确认逻辑发布确认的策略单个确认发布批量确认发布异步确认发布 发布确认逻辑 生产者将信道设置成 confirm 模式&#xff0c;一旦信道进入 confirm 模式&#xff0c;所有在该信道上面发布的消息都将会被指派一个唯一的 ID(从 1 开始)&#xff0c;一旦消…

什么时候 MySQL 查询会变慢?

前面几篇文章和小伙伴们聊的基本上都是从索引的角度去优化 MySQL 查询&#xff0c;然而&#xff0c;索引创建的好&#xff0c;并不意味着查询就一定快&#xff0c;影响查询效率的因素特别多&#xff0c;今天我们就来聊一聊这些可能影响到查询的因素。 1. 查询流程 开始今天的…

欢迎来到新世界

&#xff08;1&#xff09; 我去年对技术的发展是比较灰心的&#xff1a; 云原生&#xff1a;技术一直动荡&#xff0c;SOA->Servless、Docker->WASM、GitOpsCICDDevOps云计算&#xff1a;在中国从公有云走向了私有云&#xff0c;乃至金融云、国资云、政务云等等N种云Saa…

圆满收官!飞桨黑客松第四期高手云集,四大赛道开源贡献持续升级

2023年2月20日PaddlePaddle Hackathon 飞桨黑客马拉松&#xff08;以下简称为“飞桨黑客松”&#xff09;第四期活动发布后&#xff0c;开发者们反响热烈&#xff0c;围绕四大赛道展开了激烈角逐&#xff0c;超过2000位社区开发者参与到飞桨黑客松中&#xff0c;完成800余次任务…

直播教学签到功能(互动功能接收端JS-SDK)

功能概述 本模块主要用于接收和处理讲师、助教和管理员等用户发起的签到操作。 初始化及销毁 在实例化该模块并进行使用之前&#xff0c;需要对SDK进行初始化配置&#xff0c;详细见参考文档。 在线文件引入方式 // script 标签引入&#xff0c;根据版本号引入JS版本。 <…

ChatGPT 和 Bing Chat两者之间的比较,看完你就懂了

目录 一、ChatGPT 1.1 介绍 1.2 特点 1.3 使用场景 二、 Bing Chat 2.1 介绍 2.2 功能特点 2.3 使用场景 三、对比 一、ChatGPT 1.1 介绍 ChatGPT是一款基于人工智能技术的语言模型应用&#xff0c;由美国人工智能研究实验室OpenAI在2022年11月30日推出。该模型是一种…

【深度学习】跌倒识别 Yolov5(带数据集和源码)从0到1,内含很多数据处理的坑点和技巧,收获满满

文章目录 前言1. 数据集1.1 数据初探1.2 数据处理1.3 训练前验证图片1.4 翻车教训和进阶知识 2. 训练3.效果展示 前言 又要到做跌倒识别了。 主流方案有两种&#xff1a; 1.基于关键点的识别&#xff0c;然后做业务判断&#xff0c;判断跌倒&#xff0c;用openpose可以做到。…

干货分享 | CloudQuery 数据保护能力之动态数据脱敏!

在企业数字化转型的过程中&#xff0c;尤其随着互联网、云计算、大数据等信息技术与通信技术的迅猛发展&#xff0c;海量数据在各种信息系统上被存储和处理&#xff0c;其中包含大量有价值的敏感数据&#xff0c;这意味着数据泄露的风险也不断增加。 数据泄露可能由各种因素引…

【项目】实现web服务器

目录 1.需要实现的项目需求&#xff08;web服务器的工作原理&#xff09; 2.实现过程&#xff1a; 1.编写套接字 2.多线程的代码和任务类 3.文件描述符的处理方法的框架 4.读取请求 4.1.读取请求行 4.2.读取请求报头 4.3.分析请求行和报头 请求行的方法、URI、版本…