图(2)--加权有向图和最小生成树
一、有向图
- 定义:有向图是一副具有方向性的图,是由一组顶点和一组有方向的边组成的,每条方向的边都连着一对有序的顶点。
- 出度:由某个顶点指出的边的个数称为该顶点的出度。
- 入度:指向某个顶点的边的个数称为该顶点的入度。
- 有向路径:由一系列顶点组成,对于其中的每个顶点都存在一条有向边,从它指向序列中的下一个顶点。
- 有向环:一条至少含有一条边,且起点和终点相同的有向路径。
- 路径或环的长度即为其中包含的边数
package 图; import java.util.LinkedList; import java.util.Queue; public class Digraph { private int V;// 顶点 private int E;// 边 private Queue[] adj;// 邻接表 //构造方法,创建一副含有V个顶点但没有边的有向图 public Digraph(int v) { this.V = v; this.E=0; this.adj=new Queue[V]; for (int i = 0; i ) { adj[i]=new LinkedList<>(); } } // 获取边的总数 public int getE() { return E; } // 获取顶点总数 public int getV() { return V; } // 向有向图添加一条v-w的边 public void addEdge(int v,int w){ adj[v].add(w); E++; } public void addEdge(int... Edges){ if(Edges.length%2==0){ for (int i = 0; i ) { addEdge(Edges[i],Edges[i+1]); } }else throw new IllegalStateException("顶点数必须为2的整数倍"); } // 获取v指出的边所连接的所有顶点 public Queue adj(int V) { return adj[V]; } // 获取图的反向图 private Digraph reverse(){ Digraph digraph = new Digraph(V); for (int i = 0; i ) { for (Integer integer : adj(i)) { digraph.addEdge(integer,i); } } return digraph; } // 输出 @Override public String toString() { StringBuilder s = new StringBuilder("(" + V + " vertices, " + E + " edges)\n"); for (int v = 0; v < V; v++) { s.append(v).append(": "); for (int w : this.adj(v)) { s.append(w).append(" "); } s.append("\n"); } return s.toString(); } }
- 可达性:判断其他顶点和给定的起点是否连通。利用深度优先搜索算法解决。在有向图中,dfs标记由一个集合的顶点可达的所有顶点所需的时间与被标记的所有顶点的初度之和成正比。
二、拓扑排序
给定一幅有向图,将所有的顶点排序,使得所有的有向边均从排在前面的元素指向排在后面的元素
- 有向环检测:1. 如果当前顶点正在搜索,且没被访问过,进行深部优先搜索,并入栈onstack[i]=true 2.如果当前顶点搜索完毕,则把对应的onStack数组中的值改为false,标识出栈 3. 如果即将要搜索某个顶点,但该顶点已经在栈中,则图中有环
package 图; import java.util.Stack; public class DigraphDetectedCycle { private int[] edgeTo;//标记上一条边 private boolean[] visited;//标记已被访问过 private Stackcycle;//有向环中的所有顶点 private boolean[] onStack;//递归调用的栈上的所有顶点 public DigraphDetectedCycle(Digraph G,int v) { this.edgeTo=new int[G.getV()]; visited=new boolean[G.getV()]; onStack=new boolean[G.getV()]; for (int i = 0; i ) { if(!visited[i]){ dfs(G,i); } } } //有向环检测 public void dfs(Digraph G,int v){ onStack[v]=true; visited[v]=true; for (Integer w : G.adj(v)) { if(this.hasCycle()) return; else if(!visited[v]){ edgeTo[w]=v; dfs(G,w); }else if(onStack[w]){ //如果有环 cycle=new Stack<>(); for (int x = v; x !=w; x=edgeTo[x]) { cycle.push(x); //把有向环涉及的顶点入栈,并将该顶点值为false,说明该顶点处于有向环上 } cycle.push(w); cycle.push(v); } onStack[v]=false; } } private boolean hasCycle() { return cycle!=null; } }
- 基于深度优先的顶点排序:当且仅当一幅有向图是无环的才能进行优先排序。
- 前序:递归调用之前将顶点加入队列
- 后序:递归调用之后将顶点加入队列
- 逆后序:递归调用之后将顶点加入栈
//有向环检测 public void dfs(Digraph G,int v){ onStack[v]=true; visited[v]=true; pre.offer(v); for (Integer w : G.adj(v)) { if(!visited[v]){ edgeTo[w]=v; dfs(G,w); } }
post.offer(v);
reversePost.push(v); } private boolean hasCycle() { return cycle!=null; } Queuepre; Queue post; Stack reversePost;
- 拓扑排序:先判断是否有环,无环进行优先排序。他是所有顶点的逆后序排列,从排名较前的顶点指向排名较后的顶点。所需的时间和V+E成正比
package 图; import java.util.Stack; public class TopoLogical { // 顶点的拓扑排序 Stackorder; // 构建拓扑对象 public TopoLogical(Digraph digraph) { //创建一个检测有向环的对象 DigraphDetectedCycle cycle = new DigraphDetectedCycle(digraph); //判断G图中有没有环,如果没有环,则进行顶点排序:创建一个顶点排序对象 if (!cycle.hasCycle()){ order = cycle.reversePost; } } public Stack order(){ return order; } public static void main(String[] args) { //准备有向图 Digraph digraph = new Digraph(6); digraph.addEdge(0,2); digraph.addEdge(0,3); digraph.addEdge(2,4); digraph.addEdge(3,4); digraph.addEdge(4,5); digraph.addEdge(1,3); //通过TopoLogical对象堆有向图中的顶点进行排序 TopoLogical topoLogical = new TopoLogical(digraph); //获取顶点的线性序列进行打印 Stack order = topoLogical.order(); //进行字符串拼接 StringBuilder sb = new StringBuilder(); for (Integer w : order) { //为了最后不出现横杠和箭头,用字符串拼接 //System.out.println(w+"->"); sb.append(w+"->");//优化代码 } String str = sb.toString(); //标记最后的横杠和箭头 int index = str.lastIndexOf("->"); //截取最后横杠和箭头 0到index,最后输出即可 str = str.substring(0,index); System.out.println(str); } }
- 有向图的强连通性:如果两个顶点v,w是互相可达的,称它们是强连通的,如果任意一幅图的两个顶点都是强连通的称有向图是强连通的,一定有环
- 强连通分量:自反性,对称性,传递性
- Kosaraju算法
- 在给定的一幅有向图中,使用深度优先搜索来计算他的反向图的逆后序排列
- 在有向图中进行标准的深度优先搜索,按照刚才计算的顺序访问未被标记的顶点
- 在构造函数中,所有在同一个递归调用中被访问到的顶点都在同一个强连通分量中
package 图; public class Kosaraju { int[] id;//前连通分类的标识符 int count;//前连通分类的数量 boolean[] visited;//已访问过的顶点 public Kosaraju(Digraph digraph) { visited=new boolean[digraph.getV()]; id=new int[digraph.getV()]; DigraphDetectedCycle order = new DigraphDetectedCycle(digraph.reverse()); for (Integer v : order.reversePost) { if(!visited[v]){ dfs(digraph,v); count++; } } } private void dfs(Digraph digraph, Integer v) { visited[v]=true; id[v]=count; for (Integer w : digraph.adj(v)) { if(!visited[w]){ dfs(digraph,w); } } } public boolean stronglyConnected(int v, int w){ return id[v]==id[w]; } public int id(int v){ return id[v]; } public int getCount(){ return count; } }
三、最小生成树
- 加权图:一种为每条边关联一个权值或是成本的图模型
- 加权无向图表示
package 图; public class Edge implements Comparable{ public int v; public int w; public double weight; public Edge(int v, int w,double weight) { this.v=v; this.w=w; this.weight = weight; } public int getV() { return v; } public void setV(int v) { this.v = v; } public int getW() { return w; } public void setW(int w) { this.w = w; } public double getWeight() { return weight; } public void setWeight(double weight) { this.weight = weight; } //获取另一个顶点 public int order(int vertex){ if(v==vertex)return w; if(w==vertex)return v; else throw new RuntimeException("Inconsistent edge"); } @Override public int compareTo(Edge o) { if(this.weight return -1; else if(this.weight==o.weight)return 0;else return 1; } @Override public String toString() { return String.format("%d-%d %.2f",v,w,weight); } }
package 图; import javax.swing.text.AbstractDocument; import java.util.LinkedList; import java.util.Queue; public class EdgeWeightGraph { public int V; public int E; public Queue[] adj;//邻接表 public EdgeWeightGraph(int v) { V = v; E=0; adj=new Queue[v]; for (Queue edges : adj) { edges=new LinkedList<>(); } } public int getV() { return V; } public int getE() { return E; } //添加边及权重 public void addEdge(Edge edge){ adj[edge.v].offer(edge); adj[edge.w].offer(edge); E++; } public Queue adj(int v){ return adj[v]; } }
- 最小生成树:图的生成树是它的一刻含有其所有顶点的无环连通子图。一幅加权图的最小生成树MST 是他的一棵权值和最小的生成树
- 用一条边连接树中的任意两个顶点都会产生一个新的环
- 从树中删去一条边将会得到两棵独立的树
- 最小生成树只可能存在于连通图中;边的权重不一定表示距离;边的权重可能是0或者负数;所有边的权重都各不相同
- 切分定理:在一幅加权图中,给定任意的切分,他的横切边中的权重最小者必然属于图的最小生成树
- 切分:将图的所有顶点按照某些规则分为两个非空且没有交集的集合。
- 横切边:连接两个属于不同集合的顶点的边称之为横切边。
- 权重最小的横切边并不一定是所有横切边中唯一属于图的最小生成树的边
- 贪心算法:将含有V个顶点的任意加权连通图中属于最小生成树的边标记为黑色,初始状态下所有边均为灰色,找到一种切分,它产生的横切边均不为黑色,将它权重最小的横切边标记为黑色,反复知道标记了V-1边
- 计算最小生成树的两种算法:Prim算法和Kruskal算法,只能处理加权无向图
四、Prim算法
- Prim算法能得到任意加权连通图的最小生成树,每一步为一棵生长中的数添加一条边,每次总是将下一条连接树中的顶点与不在树中的顶点且权重最小的边加入树中。让最小索引优先队列的索引值表示图的顶点,让最小索引优先队列中的值表示从其他某个顶点到当前顶点的边权重
- Prim算法的延时实现计一幅含有V个顶点和E条边的连通加权无向图的最小生成树所需的空间与E成正比,所需的时间与ElogE成正比
package 图; import java.util.Comparator; import java.util.Iterator; import java.util.NoSuchElementException; import java.util.function.Consumer; //Key 泛型类型 public class MinPQimplements Iterable { private Key[] pq; // 索引 private int n; // number of items on priority queue private Comparator comparator; // optional comparator /** 指定容量的初始化 */ public MinPQ(int initCapacity) { pq = (Key[]) new Object[initCapacity + 1]; n = 0; } /** * 空队列的初始化 */ public MinPQ() { this(1); } /** 基于给定的容量和比较器规则初始化队列 */ public MinPQ(int initCapacity, Comparator comparator) { this.comparator = comparator; pq = (Key[]) new Object[initCapacity + 1]; n = 0; } /** 基于比较器规则初始化队列 */ public MinPQ(Comparator comparator) { this(1, comparator); } /** 从键数组初始化优先队列。 */ public MinPQ(Key[] keys) { n = keys.length; pq = (Key[]) new Object[keys.length + 1]; for (int i = 0; i < n; i++) pq[i+1] = keys[i]; for (int k = n/2; k >= 1; k--) sink(k); assert isMinHeap(); } /** 判断优先队列是否为空 */ public boolean isEmpty() { return n == 0; } /** 返回容量 */ public int size() { return n; } /** 返回此优先队列上最小的键值。 */ public Key min() { if (isEmpty()) throw new NoSuchElementException("Priority queue underflow"); return pq[1]; } // 调整基础数组的大小以具有给定的容量 private void resize(int capacity) { assert capacity > n; Key[] temp = (Key[]) new Object[capacity]; for (int i = 1; i <= n; i++) { temp[i] = pq[i]; } pq = temp; } /** 向此优先队列添加一个新键 */ public void insert(Key x) { // double size of array if necessary if (n == pq.length - 1) resize(2 * pq.length); // add x, and percolate it up to maintain heap invariant pq[++n] = x; swim(n); assert isMinHeap(); } /** 删除并返回此优先队列上最小的键值。 */ public Key delMin() { if (isEmpty()) throw new NoSuchElementException("Priority queue underflow"); Key min = pq[1]; exch(1, n--); sink(1); pq[n+1] = null; // to avoid loitering and help with garbage collection if ((n > 0) && (n == (pq.length - 1) / 4)) resize(pq.length / 2); assert isMinHeap(); return min; } /*************************************************************************** * Helper functions to restore the heap invariant. ***************************************************************************/ private void swim(int k) { while (k > 1 && greater(k/2, k)) { exch(k, k/2); k = k/2; } } private void sink(int k) { while (2*k <= n) { int j = 2*k; if (j < n && greater(j, j+1)) j++; if (!greater(k, j)) break; exch(k, j); k = j; } } /*************************************************************************** * Helper functions for compares and swaps. ***************************************************************************/ private boolean greater(int i, int j) { if (comparator == null) { return ((Comparable ) pq[i]).compareTo(pq[j]) > 0; } else { return comparator.compare(pq[i], pq[j]) > 0; } } private void exch(int i, int j) { Key swap = pq[i]; pq[i] = pq[j]; pq[j] = swap; } // 最小堆 private boolean isMinHeap() { for (int i = 1; i <= n; i++) { if (pq[i] == null) return false; } for (int i = n+1; i < pq.length; i++) { if (pq[i] != null) return false; } if (pq[0] != null) return false; return isMinHeapOrdered(1); } // 最小堆的子堆 private boolean isMinHeapOrdered(int k) { if (k > n) return true; int left = 2*k; int right = 2*k + 1; if (left <= n && greater(k, left)) return false; if (right <= n && greater(k, right)) return false; return isMinHeapOrdered(left) && isMinHeapOrdered(right); } /** 返回一个迭代器,迭代该优先级队列上的键 */ public Iterator iterator() { return new HeapIterator(); } private class HeapIterator implements Iterator { // create a new pq private MinPQ copy; // add all items to copy of heap // takes linear time since already in heap order so no keys move public HeapIterator() { if (comparator == null) copy = new MinPQ (size()); else copy = new MinPQ (size(), comparator); for (int i = 1; i <= n; i++) copy.insert(pq[i]); } public boolean hasNext() { return !copy.isEmpty(); } public void remove() { throw new UnsupportedOperationException(); } public Key next() { if (!hasNext()) throw new NoSuchElementException(); return copy.delMin(); } } }
package 图; import java.util.LinkedList; import java.util.PriorityQueue; import java.util.Queue; public class LazyPrimMST { private boolean[] marked;//最小生成树的顶点 private Queuemst;//最小生成树的边 private MinPQ pq;//横切边(包括失效的边),会自动排列 public LazyPrimMST(EdgeWeightGraph graph) { pq=new MinPQ<>(); marked=new boolean[graph.getV()]; mst=new LinkedList<>(); visit(graph,0); while (!pq.isEmpty()){ Edge e=pq.delMin();//从队列中取出最小权重的边 int v=e.v,w=e.w; if(marked[v]&&marked[w])continue;//跳过失效的边 mst.offer(e);//将边添加到数组 if(!marked[v])visit(graph,v); if(!marked[w])visit(graph,w);//将顶点v或w添加到树中 } } private void visit(EdgeWeightGraph graph, int w) { //标记顶点w并将所有连接w和未被标记的顶点的边加入pq marked[w]=true; for (Edge edge : graph.adj(w)) { if(!marked[edge.w]) pq.insert(edge); } } public Queue edges(){ return mst; } }
- Prim 的即时实现:计算一幅含有V个顶点和E条边的连通加权无向图的最小生成树所需的空间和V成正比,所需的时间和ElogV成正比
package 图; import java.util.Iterator; import java.util.NoSuchElementException; public class IndexMinPQextends Comparable > implements Iterable { private int maxN; // maximum number of elements on PQ private int n; // number of elements on PQ private int[] pq; // binary heap using 1-based indexing private int[] qp; // inverse of pq - qp[pq[i]] = pq[qp[i]] = i private Key[] keys; // keys[i] = priority of i /** 初始化 */ public IndexMinPQ(int maxN) { if (maxN < 0) throw new IllegalArgumentException(); this.maxN = maxN; n = 0; keys = (Key[]) new Comparable[maxN + 1]; // make this of length maxN?? pq = new int[maxN + 1]; qp = new int[maxN + 1]; // make this of length maxN?? for (int i = 0; i <= maxN; i++) qp[i] = -1; } /** 队列是否为空 */ public boolean isEmpty() { return n == 0; } /** 是否绑定了索引 */ public boolean contains(int i) { validateIndex(i); return qp[i] != -1; } /** 返回容量 */ public int size() { return n; } /** 插入 */ public void insert(int i, Key key) { validateIndex(i); if (contains(i)) throw new IllegalArgumentException("index is already in the priority queue"); n++; qp[i] = n; pq[n] = i; keys[i] = key; swim(n); } /** *返回最小值的索引 */ public int minIndex() { if (n == 0) throw new NoSuchElementException("Priority queue underflow"); return pq[1]; } /** 返回最小值 */ public Key minKey() { if (n == 0) throw new NoSuchElementException("Priority queue underflow"); return keys[pq[1]]; } /** 移除最小值 */ public int delMin() { if (n == 0) throw new NoSuchElementException("Priority queue underflow"); int min = pq[1]; exch(1, n--); sink(1); assert min == pq[n+1]; qp[min] = -1; // delete keys[min] = null; // to help with garbage collection pq[n+1] = -1; // not needed return min; } /** 返回值对应的索引 */ public Key keyOf(int i) { validateIndex(i); if (!contains(i)) throw new NoSuchElementException("index is not in the priority queue"); else return keys[i]; } /** 改变索引 */ public void changeKey(int i, Key key) { validateIndex(i); if (!contains(i)) throw new NoSuchElementException("index is not in the priority queue"); keys[i] = key; swim(qp[i]); sink(qp[i]); } /** 改变索引下的值 */ @Deprecated public void change(int i, Key key) { changeKey(i, key); } /** 将索引关联的键值减小到指定的值 */ public void decreaseKey(int i, Key key) { validateIndex(i); if (!contains(i)) throw new NoSuchElementException("index is not in the priority queue"); if (keys[i].compareTo(key) == 0) throw new IllegalArgumentException("Calling decreaseKey() with a key equal to the key in the priority queue"); if (keys[i].compareTo(key) < 0) throw new IllegalArgumentException("Calling decreaseKey() with a key strictly greater than the key in the priority queue"); keys[i] = key; swim(qp[i]); } /** * Increase the key associated with index {@code i} to the specified value. * * @param i the index of the key to increase * @param key increase the key associated with index {@code i} to this key * @throws IllegalArgumentException unless {@code 0 <= i < maxN} * @throws IllegalArgumentException if {@code key <= keyOf(i)} * @throws NoSuchElementException no key is associated with index {@code i} */ public void increaseKey(int i, Key key) { validateIndex(i); if (!contains(i)) throw new NoSuchElementException("index is not in the priority queue"); if (keys[i].compareTo(key) == 0) throw new IllegalArgumentException("Calling increaseKey() with a key equal to the key in the priority queue"); if (keys[i].compareTo(key) > 0) throw new IllegalArgumentException("Calling increaseKey() with a key strictly less than the key in the priority queue"); keys[i] = key; sink(qp[i]); } /** 删除键值 */ public void delete(int i) { validateIndex(i); if (!contains(i)) throw new NoSuchElementException("index is not in the priority queue"); int index = qp[i]; exch(index, n--); swim(index); sink(index); keys[i] = null; qp[i] = -1; } // throw an IllegalArgumentException if i is an invalid index private void validateIndex(int i) { if (i < 0) throw new IllegalArgumentException("index is negative: " + i); if (i >= maxN) throw new IllegalArgumentException("index >= capacity: " + i); } /*************************************************************************** * General helper functions. ***************************************************************************/ private boolean greater(int i, int j) { return keys[pq[i]].compareTo(keys[pq[j]]) > 0; } private void exch(int i, int j) { int swap = pq[i]; pq[i] = pq[j]; pq[j] = swap; qp[pq[i]] = i; qp[pq[j]] = j; } /*************************************************************************** * Heap helper functions. ***************************************************************************/ private void swim(int k) { while (k > 1 && greater(k/2, k)) { exch(k, k/2); k = k/2; } } private void sink(int k) { while (2*k <= n) { int j = 2*k; if (j < n && greater(j, j+1)) j++; if (!greater(k, j)) break; exch(k, j); k = j; } } /*************************************************************************** * Iterators. ***************************************************************************/ /** * Returns an iterator that iterates over the keys on the * priority queue in ascending order. * The iterator doesn't implement {@code remove()} since it's optional. * * @return an iterator that iterates over the keys in ascending order */ public Iterator iterator() { return new HeapIterator(); } private class HeapIterator implements Iterator { // create a new pq private IndexMinPQ copy; // add all elements to copy of heap // takes linear time since already in heap order so no keys move public HeapIterator() { copy = new IndexMinPQ (pq.length - 1); for (int i = 1; i <= n; i++) copy.insert(pq[i], keys[pq[i]]); } public boolean hasNext() { return !copy.isEmpty(); } public void remove() { throw new UnsupportedOperationException(); } public Integer next() { if (!hasNext()) throw new NoSuchElementException(); return copy.delMin(); } } }
package 图; public class PrimMST { private Edge[] edges;//距离树最近的边 private double[] disTo;//disTo[w]=edges[w].weight; private boolean[] marked; private IndexMinPQpq;//有效的横切边 public PrimMST(EdgeWeightGraph graph) { edges=new Edge[graph.getV()]; disTo=new double[graph.getV()]; marked=new boolean[graph.getV()]; pq=new IndexMinPQ<>(graph.getV()); for (int i = 0; i ) { disTo[i]=Double.POSITIVE_INFINITY; } disTo[0]=0.0; pq.insert(0,0.0);//用顶点0和权重9初始化pq while (!pq.isEmpty()){ visit(graph,pq.delMin());//将最近的顶点添加到树中 } } //将顶点v添加到树中更新数据 private void visit(EdgeWeightGraph graph, int v) { marked[v]=true; for (Edge edge : graph.adj(v)) { int w=edge.w; if(marked[w]) continue;; if(edge.weight<disTo[w]){ //连接w和树的最佳边变为edge edges[w]=edge; disTo[w]=edge.weight; if(pq.contains(w)){ pq.changeKey(w,disTo[w]); }else { pq.insert(w,disTo[w]); } } } } }
五、Kruskal算法
- 按照边的权重顺序从小到大处理,将边加入最小生成树中,加入的边不会与已加入的边构成环,直到树中有V-1条边为止
- 计算一幅含有V个顶点和E条边的连通加权无向图的最小生成树所需的空间和E成正比,所需的时间和ElogE成正比
- 区别: Prim是一个顶点一条边的找,接着从边的另一个顶点依次进行;Kruskal是将所有边排序,对边进行组合,是否遍历了所有的顶点;Kruskal算法一般比Prim慢,因为在处理每条边是除了都用到了优先队列,K算法还需要进行联合connect操作
package 图; //并查集(Union-Find)是解决动态连通性问题的一类非常高效的数据结构 public class UF { private int[] parent; // parent[i] = parent of i private byte[] rank; // rank[i] = rank of subtree rooted at i (never more than 31) private int count; // number of components /** 初始化 */ public UF(int n) { if (n < 0) throw new IllegalArgumentException(); count = n; parent = new int[n]; rank = new byte[n]; for (int i = 0; i < n; i++) { parent[i] = i; rank[i] = 0; } } /** 返回包含元素的集合的规范元素 */ public int find(int p) { validate(p); while (p != parent[p]) { parent[p] = parent[parent[p]]; // path compression by halving p = parent[p]; } return p; } /** 返回集合数量 */ public int count() { return count; } /** 如果两个元素在统一集合返回真 */ @Deprecated public boolean connected(int p, int q) { return find(p) == find(q); } /** 合并两个集合的元素 */ public void union(int p, int q) { int rootP = find(p); int rootQ = find(q); if (rootP == rootQ) return; // make root of smaller rank point to root of larger rank if (rank[rootP] < rank[rootQ]) parent[rootP] = rootQ; else if (rank[rootP] > rank[rootQ]) parent[rootQ] = rootP; else { parent[rootQ] = rootP; rank[rootP]++; } count--; } // validate that p is a valid index private void validate(int p) { int n = parent.length; if (p < 0 || p >= n) { throw new IllegalArgumentException("index " + p + " is not between 0 and " + (n - 1)); } } }
package 图; import java.util.LinkedList; import java.util.Queue; public class Kruskal { private Queuemst;//最小生成树 public Kruskal(EdgeWeightGraph graph) { mst=new LinkedList<>(); MinPQ pq=new MinPQ<>(); for (int i = 0; i ) { Queue adj = graph.adj(i); for (Edge edge : adj) { pq.insert(edge); } } UF uf = new UF(graph.getV()); while (!pq.isEmpty()){ Edge edge = pq.delMin(); int v=edge.v; int w=edge.w; if(uf.connected(v,w)) continue;//如果已经在一条边的两个顶点,则忽略失效边 uf.union(v,w);//否则就合并分量 mst.offer(edge);//添加如队列 } } }