一、思想
1.1 基本概念
- 加权无向图的生成树:一棵含有其所有顶点的无环连通子图。
- 最小生成树(MST):一棵权值最小(树中所有边的权值之和)的生成树。
1.2 算法原理
1.2.1 切分定理
- 切分定义:图的一种切分是将图的所有顶点分为两个非空且不重合的两个集合。横切边是一条连接两个属于不同集合的顶点的边。
- 切分定理:在一幅加权图中,给定任意的切分,它的横切边中的权重最小者必然属于图的最小生成树。
1.2.2 算法原理
切分定理是解决最小生成树问题的所有算法的基础。切分定理再结合贪心算法思想,就可以最终落地实现最小生成树。
Prim算法原理:
一开始树中只有一个顶点,向它添加v-1条边,每次总是将下一条连接 “树中的顶点” 与 “不在树中的顶点” 且权重最小的边,加入树中。如下图,当我们将顶点v添加到树中时,可能使得w到最小生成树的距离更近了(然后遍历顶点v的领接链表即可)。
核心:
使用一个索引优先队列,保存每个非树顶点w的一条边(将它与树中顶点连接起来的权重最小的边)。优先队列(小顶堆)的最小键即是权重最小的横切边的权重,而和它相关联的顶点V就是下一个将被添加到树中的顶点。
二、实现
2.1 无向边
1 package study.algorithm.graph; 2 3 import study.algorithm.base.StdOut; 4 5 /*** 6 * @Description 无向边 7 * @author denny.zhang 8 * @date 2020/5/25 10:34 上午 9 */ 10 public class Edge implements Comparable<Edge> { 11 12 /** 13 * 一个顶点 14 */ 15 private final int v; 16 /** 17 * 另一个顶点 18 */ 19 private final int w; 20 /** 21 * 权重 22 */ 23 private final double weight; 24 25 /** 26 * Initializes an edge between vertices {@code v} and {@code w} of 27 * the given {@code weight}. 28 * 29 * @param v one vertex 30 * @param w the other vertex 31 * @param weight the weight of this edge 32 * @throws IllegalArgumentException if either {@code v} or {@code w} 33 * is a negative integer 34 * @throws IllegalArgumentException if {@code weight} is {@code NaN} 35 */ 36 public Edge(int v, int w, double weight) { 37 if (v < 0) throw new IllegalArgumentException("vertex index must be a nonnegative integer"); 38 if (w < 0) throw new IllegalArgumentException("vertex index must be a nonnegative integer"); 39 if (Double.isNaN(weight)) throw new IllegalArgumentException("Weight is NaN"); 40 this.v = v; 41 this.w = w; 42 this.weight = weight; 43 } 44 45 /** 46 * Returns the weight of this edge. 47 * 48 * @return the weight of this edge 49 */ 50 public double weight() { 51 return weight; 52 } 53 54 /** 55 * 返回边的任意一个顶点 56 * 57 * @return either endpoint of this edge 58 */ 59 public int either() { 60 return v; 61 } 62 63 /** 64 * 返回边的另一个顶点 65 * 66 * @param vertex one endpoint of this edge 67 * @return the other endpoint of this edge 68 * @throws IllegalArgumentException if the vertex is not one of the 69 * endpoints of this edge 70 */ 71 public int other(int vertex) { 72 if (vertex == v) return w; 73 else if (vertex == w) return v; 74 else throw new IllegalArgumentException("Illegal endpoint"); 75 } 76 77 /** 78 * Compares two edges by weight. 79 * Note that {@code compareTo()} is not consistent with {@code equals()}, 80 * which uses the reference equality implementation inherited from {@code Object}. 81 * 82 * @param that the other edge 83 * @return a negative integer, zero, or positive integer depending on whether 84 * the weight of this is less than, equal to, or greater than the 85 * argument edge 86 */ 87 @Override 88 public int compareTo(Edge that) { 89 return Double.compare(this.weight, that.weight); 90 } 91 92 /** 93 * Returns a string representation of this edge. 94 * 95 * @return a string representation of this edge 96 */ 97 public String toString() { 98 return String.format("%d-%d %.5f", v, w, weight); 99 } 100 101 /** 102 * Unit tests the {@code Edge} data type. 103 * 104 * @param args the command-line arguments 105 */ 106 public static void main(String[] args) { 107 Edge e = new Edge(12, 34, 5.67); 108 StdOut.println(e); 109 StdOut.println("任意一个顶点="+e.either()); 110 StdOut.println("另一个顶点="+e.other(12)); 111 } 112 }
如上图,初始化时构造了一个邻接表。Bag<Edge>[] adj,如下图。每条边有2个顶点,所以插入adj[]中2次。
2.2.边加权无向图
1 package study.algorithm.graph; 2 3 import study.algorithm.base.*; 4 5 import java.util.NoSuchElementException; 6 7 /*** 8 * @Description 边权重无向图 9 * @author denny.zhang 10 * @date 2020/5/25 10:50 上午 11 */ 12 public class EdgeWeightedGraph { 13 private static final String NEWLINE = System.getProperty("line.separator"); 14 15 /** 16 * 顶点数 17 */ 18 private final int V; 19 /** 20 * 边数 21 */ 22 private int E; 23 /** 24 * 顶点邻接表,每个元素Bag代表:由某个顶点关联的边数组,按顶点顺序排列 25 */ 26 private Bag<Edge>[] adj; 27 28 /** 29 * Initializes an empty edge-weighted graph with {@code V} vertices and 0 edges. 30 * 31 * @param V the number of vertices 32 * @throws IllegalArgumentException if {@code V < 0} 33 */ 34 public EdgeWeightedGraph(int V) { 35 if (V < 0) throw new IllegalArgumentException("Number of vertices must be nonnegative"); 36 this.V = V; 37 this.E = 0; 38 adj = (Bag<Edge>[]) new Bag[V]; 39 for (int v = 0; v < V; v++) { 40 adj[v] = new Bag<Edge>(); 41 } 42 } 43 44 /** 45 * Initializes a random edge-weighted graph with {@code V} vertices and <em>E</em> edges. 46 * 47 * @param V the number of vertices 48 * @param E the number of edges 49 * @throws IllegalArgumentException if {@code V < 0} 50 * @throws IllegalArgumentException if {@code E < 0} 51 */ 52 public EdgeWeightedGraph(int V, int E) { 53 this(V); 54 if (E < 0) throw new IllegalArgumentException("Number of edges must be nonnegative"); 55 for (int i = 0; i < E; i++) { 56 int v = StdRandom.uniform(V); 57 int w = StdRandom.uniform(V); 58 double weight = Math.round(100 * StdRandom.uniform()) / 100.0; 59 Edge e = new Edge(v, w, weight); 60 addEdge(e); 61 } 62 } 63 64 /** 65 * Initializes an edge-weighted graph from an input stream. 66 * The format is the number of vertices <em>V</em>, 67 * followed by the number of edges <em>E</em>, 68 * followed by <em>E</em> pairs of vertices and edge weights, 69 * with each entry separated by whitespace. 70 * 71 * @param in the input stream 72 * @throws IllegalArgumentException if {@code in} is {@code null} 73 * @throws IllegalArgumentException if the endpoints of any edge are not in prescribed range 74 * @throws IllegalArgumentException if the number of vertices or edges is negative 75 */ 76 public EdgeWeightedGraph(In in) { 77 if (in == null) throw new IllegalArgumentException("argument is null"); 78 79 try { 80 // 顶点数 81 V = in.readInt(); 82 // 邻接表 83 adj = (Bag<Edge>[]) new Bag[V]; 84 // 初始化邻接表,一个顶点对应一条链表 85 for (int v = 0; v < V; v++) { 86 adj[v] = new Bag<Edge>(); 87 } 88 // 边数 89 int E = in.readInt(); 90 if (E < 0) throw new IllegalArgumentException("Number of edges must be nonnegative"); 91 // 遍历每一条边 92 for (int i = 0; i < E; i++) { 93 // 一个顶点 94 int v = in.readInt(); 95 // 另一个顶点 96 int w = in.readInt(); 97 validateVertex(v); 98 validateVertex(w); 99 // 权重 100 double weight = in.readDouble(); 101 // 构造边 102 Edge e = new Edge(v, w, weight); 103 // 添加边 104 addEdge(e); 105 } 106 } 107 catch (NoSuchElementException e) { 108 throw new IllegalArgumentException("invalid input format in EdgeWeightedGraph constructor", e); 109 } 110 111 } 112 113 /** 114 * Initializes a new edge-weighted graph that is a deep copy of {@code G}. 115 * 116 * @param G the edge-weighted graph to copy 117 */ 118 public EdgeWeightedGraph(EdgeWeightedGraph G) { 119 this(G.V()); 120 this.E = G.E(); 121 for (int v = 0; v < G.V(); v++) { 122 // reverse so that adjacency list is in same order as original 123 Stack<Edge> reverse = new Stack<Edge>(); 124 for (Edge e : G.adj[v]) { 125 reverse.push(e); 126 } 127 for (Edge e : reverse) { 128 adj[v].add(e); 129 } 130 } 131 } 132 133 134 /** 135 * Returns the number of vertices in this edge-weighted graph. 136 * 137 * @return the number of vertices in this edge-weighted graph 138 */ 139 public int V() { 140 return V; 141 } 142 143 /** 144 * Returns the number of edges in this edge-weighted graph. 145 * 146 * @return the number of edges in this edge-weighted graph 147 */ 148 public int E() { 149 return E; 150 } 151 152 // throw an IllegalArgumentException unless {@code 0 <= v < V} 153 private void validateVertex(int v) { 154 if (v < 0 || v >= V) 155 throw new IllegalArgumentException("vertex " + v + " is not between 0 and " + (V-1)); 156 } 157 158 /** 159 * Adds the undirected edge {@code e} to this edge-weighted graph. 160 * 161 * @param e the edge 162 * @throws IllegalArgumentException unless both endpoints are between {@code 0} and {@code V-1} 163 */ 164 public void addEdge(Edge e) { 165 // 一个顶点 166 int v = e.either(); 167 // 另一个顶点 168 int w = e.other(v); 169 validateVertex(v); 170 validateVertex(w); 171 // 追加进顶点v的邻接链表 172 adj[v].add(e); 173 // 追加进顶点w的领接链表 174 adj[w].add(e); 175 // 边数++ 176 E++; 177 } 178 179 /** 180 * 顶点V关联的全部边 181 * 182 * @param v the vertex 183 * @return the edges incident on vertex {@code v} as an Iterable 184 * @throws IllegalArgumentException unless {@code 0 <= v < V} 185 */ 186 public Iterable<Edge> adj(int v) { 187 validateVertex(v); 188 return adj[v]; 189 } 190 191 /** 192 * Returns the degree of vertex {@code v}. 193 * 194 * @param v the vertex 195 * @return the degree of vertex {@code v} 196 * @throws IllegalArgumentException unless {@code 0 <= v < V} 197 */ 198 public int degree(int v) { 199 validateVertex(v); 200 return adj[v].size(); 201 } 202 203 /** 204 * Returns all edges in this edge-weighted graph. 205 * To iterate over the edges in this edge-weighted graph, use foreach notation: 206 * {@code for (Edge e : G.edges())}. 207 * 208 * @return all edges in this edge-weighted graph, as an iterable 209 */ 210 public Iterable<Edge> edges() { 211 Bag<Edge> list = new Bag<Edge>(); 212 for (int v = 0; v < V; v++) { 213 int selfLoops = 0; 214 for (Edge e : adj(v)) { 215 if (e.other(v) > v) { 216 list.add(e); 217 } 218 // add only one copy of each self loop (self loops will be consecutive) 219 else if (e.other(v) == v) { 220 if (selfLoops % 2 == 0) list.add(e); 221 selfLoops++; 222 } 223 } 224 } 225 return list; 226 } 227 228 /** 229 * Returns a string representation of the edge-weighted graph. 230 * This method takes time proportional to <em>E</em> + <em>V</em>. 231 * 232 * @return the number of vertices <em>V</em>, followed by the number of edges <em>E</em>, 233 * followed by the <em>V</em> adjacency lists of edges 234 */ 235 public String toString() { 236 StringBuilder s = new StringBuilder(); 237 s.append(V + " " + E + NEWLINE); 238 for (int v = 0; v < V; v++) { 239 s.append(v + ": "); 240 for (Edge e : adj[v]) { 241 s.append(e + " "); 242 } 243 s.append(NEWLINE); 244 } 245 return s.toString(); 246 } 247 248 /** 249 * Unit tests the {@code EdgeWeightedGraph} data type. 250 * 251 * @param args the command-line arguments 252 */ 253 public static void main(String[] args) { 254 In in = new In(args[0]); 255 EdgeWeightedGraph G = new EdgeWeightedGraph(in); 256 StdOut.println(G); 257 } 258 259 }
2.3 索引小值优先队列
1 package study.algorithm.base; 2 3 import java.util.Iterator; 4 import java.util.NoSuchElementException; 5 6 /** 7 * 索引(顶点)最小优先级队列 8 * 9 * @param <Key> 10 */ 11 public class IndexMinPQ<Key extends Comparable<Key>> implements Iterable<Integer> { 12 /** 13 * 元素数量上限 14 */ 15 private int maxN; 16 /** 17 * 元素数量 18 */ 19 private int n; 20 /** 21 * 索引二叉堆(数组中每个元素都是顶点,顶点v,对应keys[v]):数组从pq[0]代表原点其它顶点从pq[1]开始插入 22 */ 23 private int[] pq; 24 /** 25 * 标记索引为i的元素在二叉堆中的位置。pq的反转数组(qp[index]=i):qp[pq[i]] = pq[qp[i]] = i 26 */ 27 private int[] qp; 28 29 /** 30 * 元素有序数组(按照pq的索引赋值) 31 */ 32 public Key[] keys; 33 34 /** 35 * 初始化一个空索引优先队列,索引范围:0 ~ maxN-1 36 * 37 * @param maxN the keys on this priority queue are index from {@code 0} 38 * {@code maxN - 1} 39 * @throws IllegalArgumentException if {@code maxN < 0} 40 */ 41 public IndexMinPQ(int maxN) { 42 if (maxN < 0) throw new IllegalArgumentException(); 43 this.maxN = maxN; 44 // 初始有0个元素 45 n = 0; 46 // 初始化键数组长度为maxN + 1 47 keys = (Key[]) new Comparable[maxN + 1]; 48 // 初始化"键值对"数组长度为maxN + 1 49 pq = new int[maxN + 1]; 50 // 初始化"值键对"数组长度为maxN + 1 51 qp = new int[maxN + 1]; 52 // 遍历给"值键对"数组赋值-1,后续只要!=-1,即包含i 53 for (int i = 0; i <= maxN; i++) 54 qp[i] = -1; 55 } 56 57 /** 58 * Returns true if this priority queue is empty. 59 * 60 * @return {@code true} if this priority queue is empty; 61 * {@code false} otherwise 62 */ 63 public boolean isEmpty() { 64 return n == 0; 65 } 66 67 /** 68 * Is {@code i} an index on this priority queue? 69 * 70 * @param i an index 71 * @return {@code true} if {@code i} is an index on this priority queue; 72 * {@code false} otherwise 73 * @throws IllegalArgumentException unless {@code 0 <= i < maxN} 74 */ 75 public boolean contains(int i) { 76 validateIndex(i); 77 return qp[i] != -1; 78 } 79 80 /** 81 * Returns the number of keys on this priority queue. 82 * 83 * @return the number of keys on this priority queue 84 */ 85 public int size() { 86 return n; 87 } 88 89 /** 90 * 插入一个元素,将元素key关联索引i 91 * 92 * @param i an index 93 * @param key the key to associate with index {@code i} 94 * @throws IllegalArgumentException unless {@code 0 <= i < maxN} 95 * @throws IllegalArgumentException if there already is an item associated 96 * with index {@code i} 97 */ 98 public void insert(int i, Key key) { 99 validateIndex(i); 100 if (contains(i)) throw new IllegalArgumentException("index is already in the priority queue"); 101 // 元素个数+1 102 n++; 103 // 索引为i的二叉堆位置为n 104 qp[i] = n; 105 // 二叉堆底部插入新元素,值=i 106 pq[n] = i; 107 // 索引i对应的元素赋值 108 keys[i] = key; 109 // 二叉堆中,上浮最后一个元素(小值上浮) 110 swim(n); 111 } 112 113 /** 114 * 返回最小元素的索引 115 * 116 * @return an index associated with a minimum key 117 * @throws NoSuchElementException if this priority queue is empty 118 */ 119 public int minIndex() { 120 if (n == 0) throw new NoSuchElementException("Priority queue underflow"); 121 return pq[1]; 122 } 123 124 /** 125 * 返回最小元素(key) 126 * 127 * @return a minimum key 128 * @throws NoSuchElementException if this priority queue is empty 129 */ 130 public Key minKey() { 131 if (n == 0) throw new NoSuchElementException("Priority queue underflow"); 132 return keys[pq[1]]; 133 } 134 135 /** 136 * 删除最小值key,并返回最小值(优先队列索引) 137 * 138 * @return an index associated with a minimum key 139 * @throws NoSuchElementException if this priority queue is empty 140 */ 141 public int delMin() { 142 if (n == 0) throw new NoSuchElementException("Priority queue underflow"); 143 // pq[1]即为索引最小值 144 int min = pq[1]; 145 // 交换第一个元素和最后一个元素 146 exch(1, n--); 147 // 把新换来的第一个元素下沉 148 sink(1); 149 // 校验下沉后,最后一个元素是最小值 150 assert min == pq[n+1]; 151 // 恢复初始值,-1即代表该元素已删除 152 qp[min] = -1; // delete 153 // 方便垃圾回收 154 keys[min] = null; 155 // 最后一个元素(索引)赋值-1 156 pq[n+1] = -1; // not needed 157 return min; 158 } 159 160 /** 161 * Returns the key associated with index {@code i}. 162 * 163 * @param i the index of the key to return 164 * @return the key associated with index {@code i} 165 * @throws IllegalArgumentException unless {@code 0 <= i < maxN} 166 * @throws NoSuchElementException no key is associated with index {@code i} 167 */ 168 public Key keyOf(int i) { 169 validateIndex(i); 170 if (!contains(i)) throw new NoSuchElementException("index is not in the priority queue"); 171 else return keys[i]; 172 } 173 174 /** 175 * Change the key associated with index {@code i} to the specified value. 176 * 177 * @param i the index of the key to change 178 * @param key change the key associated with index {@code i} to this key 179 * @throws IllegalArgumentException unless {@code 0 <= i < maxN} 180 * @throws NoSuchElementException no key is associated with index {@code i} 181 */ 182 public void changeKey(int i, Key key) { 183 validateIndex(i); 184 if (!contains(i)) throw new NoSuchElementException("index is not in the priority queue"); 185 keys[i] = key; 186 swim(qp[i]); 187 sink(qp[i]); 188 } 189 190 /** 191 * Change the key associated with index {@code i} to the specified value. 192 * 193 * @param i the index of the key to change 194 * @param key change the key associated with index {@code i} to this key 195 * @throws IllegalArgumentException unless {@code 0 <= i < maxN} 196 * @deprecated Replaced by {@code changeKey(int, Key)}. 197 */ 198 @Deprecated 199 public void change(int i, Key key) { 200 changeKey(i, key); 201 } 202 203 /** 204 * 减小索引i对应的值为key 205 * 更新: 206 * 1.元素数组keys[] 207 * 2.小顶二叉堆pq[] 208 * 209 * @param i the index of the key to decrease 210 * @param key decrease the key associated with index {@code i} to this key 211 * @throws IllegalArgumentException unless {@code 0 <= i < maxN} 212 * @throws IllegalArgumentException if {@code key >= keyOf(i)} 213 * @throws NoSuchElementException no key is associated with index {@code i} 214 */ 215 public void decreaseKey(int i, Key key) { 216 validateIndex(i); 217 if (!contains(i)) throw new NoSuchElementException("index is not in the priority queue"); 218 // key 值一样,报错 219 if (keys[i].compareTo(key) == 0) 220 throw new IllegalArgumentException("Calling decreaseKey() with a key equal to the key in the priority queue"); 221 // key比当前值大,报错 222 if (keys[i].compareTo(key) < 0) 223 throw new IllegalArgumentException("Calling decreaseKey() with a key strictly greater than the key in the priority queue"); 224 // key比当前值小,把key赋值进去 225 keys[i] = key; 226 // 小值上浮(qp[i]=索引i在二叉堆pq[]中的位置) 227 swim(qp[i]); 228 } 229 230 /** 231 * Increase the key associated with index {@code i} to the specified value. 232 * 233 * @param i the index of the key to increase 234 * @param key increase the key associated with index {@code i} to this key 235 * @throws IllegalArgumentException unless {@code 0 <= i < maxN} 236 * @throws IllegalArgumentException if {@code key <= keyOf(i)} 237 * @throws NoSuchElementException no key is associated with index {@code i} 238 */ 239 public void increaseKey(int i, Key key) { 240 validateIndex(i); 241 if (!contains(i)) throw new NoSuchElementException("index is not in the priority queue"); 242 if (keys[i].compareTo(key) == 0) 243 throw new IllegalArgumentException("Calling increaseKey() with a key equal to the key in the priority queue"); 244 if (keys[i].compareTo(key) > 0) 245 throw new IllegalArgumentException("Calling increaseKey() with a key strictly less than the key in the priority queue"); 246 keys[i] = key; 247 sink(qp[i]); 248 } 249 250 /** 251 * Remove the key associated with index {@code i}. 252 * 253 * @param i the index of the key to remove 254 * @throws IllegalArgumentException unless {@code 0 <= i < maxN} 255 * @throws NoSuchElementException no key is associated with index {@code i} 256 */ 257 public void delete(int i) { 258 validateIndex(i); 259 if (!contains(i)) throw new NoSuchElementException("index is not in the priority queue"); 260 int index = qp[i]; 261 exch(index, n--); 262 swim(index); 263 sink(index); 264 keys[i] = null; 265 qp[i] = -1; 266 } 267 268 // throw an IllegalArgumentException if i is an invalid index 269 private void validateIndex(int i) { 270 if (i < 0) throw new IllegalArgumentException("index is negative: " + i); 271 if (i >= maxN) throw new IllegalArgumentException("index >= capacity: " + i); 272 } 273 274 /*************************************************************************** 275 * General helper functions. 276 ***************************************************************************/ 277 private boolean greater(int i, int j) { 278 return keys[pq[i]].compareTo(keys[pq[j]]) > 0; 279 } 280 281 private void exch(int i, int j) { 282 int swap = pq[i]; 283 pq[i] = pq[j]; 284 pq[j] = swap; 285 qp[pq[i]] = i; 286 qp[pq[j]] = j; 287 } 288 289 290 /*************************************************************************** 291 * Heap helper functions. 上浮 292 ***************************************************************************/ 293 private void swim(int k) { 294 // 如果父节点值比当前节点值大,交换,父节点作为当前节点,轮询。即小值上浮。 295 while (k > 1 && greater(k/2, k)) { 296 exch(k, k/2); 297 k = k/2; 298 } 299 } 300 //下沉 301 private void sink(int k) { 302 while (2*k <= n) { 303 int j = 2*k; 304 if (j < n && greater(j, j+1)) j++; 305 if (!greater(k, j)) break; 306 exch(k, j); 307 k = j; 308 } 309 } 310 311 312 /*************************************************************************** 313 * Iterators. 314 ***************************************************************************/ 315 316 /** 317 * Returns an iterator that iterates over the keys on the 318 * priority queue in ascending order. 319 * The iterator doesn't implement {@code remove()} since it's optional. 320 * 321 * @return an iterator that iterates over the keys in ascending order 322 */ 323 @Override 324 public Iterator<Integer> iterator() { return new HeapIterator(); } 325 326 private class HeapIterator implements Iterator<Integer> { 327 // create a new pq 328 private IndexMinPQ<Key> copy; 329 330 // add all elements to copy of heap 331 // takes linear time since already in heap order so no keys move 332 public HeapIterator() { 333 copy = new IndexMinPQ<Key>(pq.length - 1); 334 for (int i = 1; i <= n; i++) 335 copy.insert(pq[i], keys[pq[i]]); 336 } 337 338 @Override 339 public boolean hasNext() { return !copy.isEmpty(); } 340 @Override 341 public void remove() { throw new UnsupportedOperationException(); } 342 343 @Override 344 public Integer next() { 345 if (!hasNext()) throw new NoSuchElementException(); 346 return copy.delMin(); 347 } 348 } 349 350 351 /** 352 * Unit tests the {@code IndexMinPQ} data type. 353 * 354 * @param args the command-line arguments 355 */ 356 public static void main(String[] args) { 357 // insert a bunch of strings 358 String[] strings = { "it", "was", "the", "best", "of", "times", "it", "was", "the", "worst" }; 359 360 IndexMinPQ<String> pq = new IndexMinPQ<String>(strings.length); 361 for (int i = 0; i < strings.length; i++) { 362 pq.insert(i, strings[i]); 363 } 364 365 // delete and print each key 366 while (!pq.isEmpty()) { 367 int i = pq.delMin(); 368 StdOut.println(i + " " + strings[i]); 369 } 370 StdOut.println(); 371 372 // reinsert the same strings 373 for (int i = 0; i < strings.length; i++) { 374 pq.insert(i, strings[i]); 375 } 376 377 // print each key using the iterator 378 for (int i : pq) { 379 StdOut.println(i + " " + strings[i]); 380 } 381 while (!pq.isEmpty()) { 382 pq.delMin(); 383 } 384 385 } 386 }
2.4 Prim算法(基于二叉堆)
1 package study.algorithm.graph; 2 3 import study.algorithm.base.*; 4 5 import java.util.Arrays; 6 7 /*** 8 * @Description 使用Prim算法计算一棵最小生成树 27 * 28 * @author denny.zhang 29 * @date 2020/5/26 9:50 上午 30 */ 31 public class PrimMST { 32 private static final double FLOATING_POINT_EPSILON = 1E-12; 33 34 /** 35 * 顶点索引,树顶点到非树顶点的最短边(距离树最近的边) 36 */ 37 private Edge[] edgeTo; 38 /** 39 * 顶点索引,最短边的权重 40 */ 41 private double[] distTo; 42 /** 43 * 顶点索引,标记顶点是否在最小生成树中 44 */ 45 private boolean[] marked; 46 /** 47 * 有效的横切边(索引最小优先队列,索引为顶点v,值pq[v]=edgeTo[v].weight()=distTo[v]) 48 */ 49 private IndexMinPQ<Double> pq; 50 51 /** 52 * 计算一个边加权图的最小生成树 53 * @param G the edge-weighted graph 54 */ 55 public PrimMST(EdgeWeightedGraph G) { 56 // 初始化3个顶点索引数组 57 edgeTo = new Edge[G.V()]; 58 distTo = new double[G.V()]; 59 marked = new boolean[G.V()]; 60 // 初始化:顶点索引最小优先队列 61 pq = new IndexMinPQ<Double>(G.V()); 62 for (int v = 0; v < G.V(); v++) { 63 // 初始化为无穷大 64 distTo[v] = Double.POSITIVE_INFINITY; 65 } 66 // 遍历顶点数 67 for (int v = 0; v < G.V(); v++) 68 { 69 // 如果顶点0开始能全进树,那就一次搞定 70 StdOut.println("v="+ v+",marked[v]="+marked[v]); 71 // 如果没进树 72 if (!marked[v]) { 73 StdOut.println("v="+v+",执行prim"); 74 // 最小生成树 75 prim(G, v); 76 } 77 } 78 79 // 校验,可省略 80 assert check(G); 81 } 82 83 84 /** 85 * 从顶点s开始生成图G 86 * @param G 87 * @param s 88 */ 89 private void prim(EdgeWeightedGraph G, int s) { 90 // 顶点s的权重=0 91 distTo[s] = 0.0; 92 // 顶点s进队列,key为索引,value为边权重 93 pq.insert(s, distTo[s]); 94 StdOut.println("顶点s进队列: s="+ s+",distTo[s]="+distTo[s]); 95 StdOut.println("pq="+ Arrays.toString(pq.keys)); 96 97 // 循环 98 while (!pq.isEmpty()) { 99 // 取出最小权重边的顶点(最近的顶点) 100 int v = pq.delMin(); 101 // 添加到树中 102 scan(G, v); 103 } 104 } 105 106 /** 107 * 将顶点V添加到树中,更新数据 108 * @param G 109 * @param v 110 */ 111 private void scan(EdgeWeightedGraph G, int v) { 112 StdOut.println("v="+ v+",进树"); 113 // 标记 进树 114 marked[v] = true; 115 // 遍历顶点v的邻接边 116 for (Edge e : G.adj(v)) { 117 StdOut.println("遍历顶点v的邻接边:v="+ v+",e="+e.toString()); 118 // 另一个顶点 119 int w = e.other(v); 120 StdOut.println("w=" + w); 121 // 如果w已进树,跳过(至少有一个点不在树中,计算才有意义) 122 if (marked[w]) { 123 StdOut.println("已进树,跳过w=" + w); 124 continue; 125 } 126 // 如果边e的权重 < 当前到顶点w的权重 127 if (e.weight() < distTo[w]) { 128 StdOut.println("e.weight()="+e.weight()+" ,distTo[w]=" + distTo[w]); 129 // 更新最小权重 130 distTo[w] = e.weight(); 131 // 连接w和树的最佳边变为e 132 edgeTo[w] = e; 133 // 顶点w在pq队列中 134 if (pq.contains(w)) { 135 StdOut.println("顶点w在pq队列中:w="+w); 136 // 减小w索引对应的权重值,小值上浮 137 pq.decreaseKey(w, distTo[w]); 138 // 顶点w不在队列中 139 } else { 140 StdOut.println("顶点w不在pq队列中,插入队列前:w="+w+",pq="+ Arrays.toString(pq.keys)); 141 // 插入队列 142 pq.insert(w, distTo[w]); 143 StdOut.println("顶点w不在pq队列中,插入队列后:w="+w+",pq="+Arrays.toString(pq.keys)); 144 } 145 } 146 } 147 } 148 149 /** 150 * Returns the edges in a minimum spanning tree (or forest). 151 * @return the edges in a minimum spanning tree (or forest) as 152 * an iterable of edges 153 */ 154 public Iterable<Edge> edges() { 155 Queue<Edge> mst = new Queue<Edge>(); 156 for (int v = 0; v < edgeTo.length; v++) { 157 Edge e = edgeTo[v]; 158 if (e != null) { 159 mst.enqueue(e); 160 } 161 } 162 return mst; 163 } 164 165 /** 166 * Returns the sum of the edge weights in a minimum spanning tree (or forest). 167 * @return the sum of the edge weights in a minimum spanning tree (or forest) 168 */ 169 public double weight() { 170 double weight = 0.0; 171 for (Edge e : edges()) { 172 weight += e.weight(); 173 } 174 return weight; 175 } 176 177 178 // check optimality conditions (takes time proportional to E V lg* V) 179 private boolean check(EdgeWeightedGraph G) { 180 181 // check weight 182 double totalWeight = 0.0; 183 for (Edge e : edges()) { 184 totalWeight += e.weight(); 185 } 186 if (Math.abs(totalWeight - weight()) > FLOATING_POINT_EPSILON) { 187 System.err.printf("Weight of edges does not equal weight(): %f vs. %f ", totalWeight, weight()); 188 return false; 189 } 190 191 // check that it is acyclic 192 UF uf = new UF(G.V()); 193 for (Edge e : edges()) { 194 int v = e.either(), w = e.other(v); 195 if (uf.find(v) == uf.find(w)) { 196 System.err.println("Not a forest"); 197 return false; 198 } 199 uf.union(v, w); 200 } 201 202 // check that it is a spanning forest 203 for (Edge e : G.edges()) { 204 int v = e.either(), w = e.other(v); 205 if (uf.find(v) != uf.find(w)) { 206 System.err.println("Not a spanning forest"); 207 return false; 208 } 209 } 210 211 // check that it is a minimal spanning forest (cut optimality conditions) 212 for (Edge e : edges()) { 213 214 // all edges in MST except e 215 uf = new UF(G.V()); 216 for (Edge f : edges()) { 217 int x = f.either(), y = f.other(x); 218 if (f != e) { 219 uf.union(x, y); 220 } 221 } 222 223 // check that e is min weight edge in crossing cut 224 for (Edge f : G.edges()) { 225 int x = f.either(), y = f.other(x); 226 if (uf.find(x) != uf.find(y)) { 227 if (f.weight() < e.weight()) { 228 System.err.println("Edge " + f + " violates cut optimality conditions"); 229 return false; 230 } 231 } 232 } 233 234 } 235 236 return true; 237 } 238 239 public static void main(String[] args) { 240 // 读取图文件 241 In in = new In(args[0]); 242 // 初始化:边加权无向图 243 EdgeWeightedGraph G = new EdgeWeightedGraph(in); 244 // 核心算法Prim 245 PrimMST mst = new PrimMST(G); 246 // 打印全部边 247 for (Edge e : mst.edges()) { 248 StdOut.println(e); 249 } 250 // 打印总权重 251 StdOut.printf("%.5f ", mst.weight()); 252 } 253 254 255 }
运行方法:
使用导入文件的方式,后续只需要修改文件内容,即可执行,比较方便。8个顶点,16条边的边加权无向图,内容如下:
8 16 4 5 0.35 4 7 0.37 5 7 0.28 0 7 0.16 1 5 0.32 0 4 0.38 2 3 0.17 1 7 0.19 0 2 0.26 1 2 0.36 1 3 0.29 2 7 0.34 6 2 0.40 3 6 0.52 6 0 0.58 6 4 0.93
运行配置:
运行:
结合邻接链表来看结果,其实就是遍历了一遍邻接表。
v=0,marked[v]=false----从顶点0作为原点,构建最小生成树,---begin! v=0,执行prim 顶点s进队列: s=0,distTo[s]=0.0 pq=[0.0, null, null, null, null, null, null, null, null] v=0,进树---》顶点0开始 另一个顶点: 6 2 4 7 遍历顶点v的邻接边:v=0,e=6-0 0.58000 w=6 e.weight()=0.58 ,distTo[w]=Infinity 顶点w不在pq队列中,插入队列前:w=6,pq=[null, null, null, null, null, null, null, null, null] 顶点w不在pq队列中,插入队列后:w=6,pq=[null, null, null, null, null, null, 0.58, null, null] 遍历顶点v的邻接边:v=0,e=0-2 0.26000 w=2 e.weight()=0.26 ,distTo[w]=Infinity 顶点w不在pq队列中,插入队列前:w=2,pq=[null, null, null, null, null, null, 0.58, null, null] 顶点w不在pq队列中,插入队列后:w=2,pq=[null, null, 0.26, null, null, null, 0.58, null, null] 遍历顶点v的邻接边:v=0,e=0-4 0.38000 w=4 e.weight()=0.38 ,distTo[w]=Infinity 顶点w不在pq队列中,插入队列前:w=4,pq=[null, null, 0.26, null, null, null, 0.58, null, null] 顶点w不在pq队列中,插入队列后:w=4,pq=[null, null, 0.26, null, 0.38, null, 0.58, null, null] 遍历顶点v的邻接边:v=0,e=0-7 0.16000 w=7 e.weight()=0.16 ,distTo[w]=Infinity 顶点w不在pq队列中,插入队列前:w=7,pq=[null, null, 0.26, null, 0.38, null, 0.58, null, null] 顶点w不在pq队列中,插入队列后:w=7,pq=[null, null, 0.26, null, 0.38, null, 0.58, 0.16, null] v=7,进树---》顶点7开始 另一个顶点: 2 1 0 5 4 遍历顶点v的邻接边:v=7,e=2-7 0.34000 w=2 遍历顶点v的邻接边:v=7,e=1-7 0.19000 w=1 e.weight()=0.19 ,distTo[w]=Infinity 顶点w不在pq队列中,插入队列前:w=1,pq=[null, null, 0.26, null, 0.38, null, 0.58, null, null] 顶点w不在pq队列中,插入队列后:w=1,pq=[null, 0.19, 0.26, null, 0.38, null, 0.58, null, null] 遍历顶点v的邻接边:v=7,e=0-7 0.16000 w=0 已进树,跳过w=0 遍历顶点v的邻接边:v=7,e=5-7 0.28000 w=5 e.weight()=0.28 ,distTo[w]=Infinity 顶点w不在pq队列中,插入队列前:w=5,pq=[null, 0.19, 0.26, null, 0.38, null, 0.58, null, null] 顶点w不在pq队列中,插入队列后:w=5,pq=[null, 0.19, 0.26, null, 0.38, 0.28, 0.58, null, null] 遍历顶点v的邻接边:v=7,e=4-7 0.37000 w=4 e.weight()=0.37 ,distTo[w]=0.38 顶点w在pq队列中:w=4 v=1,进树---》顶点1开始 另一个顶点: 3 2 7 5 遍历顶点v的邻接边:v=1,e=1-3 0.29000 w=3 e.weight()=0.29 ,distTo[w]=Infinity 顶点w不在pq队列中,插入队列前:w=3,pq=[null, null, 0.26, null, 0.37, 0.28, 0.58, null, null] 顶点w不在pq队列中,插入队列后:w=3,pq=[null, null, 0.26, 0.29, 0.37, 0.28, 0.58, null, null] 遍历顶点v的邻接边:v=1,e=1-2 0.36000 w=2 遍历顶点v的邻接边:v=1,e=1-7 0.19000 w=7 已进树,跳过w=7 遍历顶点v的邻接边:v=1,e=1-5 0.32000 w=5 v=2,进树---》顶点2开始 另一个顶点: 6 7 1 0 3 遍历顶点v的邻接边:v=2,e=6-2 0.40000 w=6 e.weight()=0.4 ,distTo[w]=0.58 顶点w在pq队列中:w=6 遍历顶点v的邻接边:v=2,e=2-7 0.34000 w=7 已进树,跳过w=7 遍历顶点v的邻接边:v=2,e=1-2 0.36000 w=1 已进树,跳过w=1 遍历顶点v的邻接边:v=2,e=0-2 0.26000 w=0 已进树,跳过w=0 遍历顶点v的邻接边:v=2,e=2-3 0.17000 w=3 e.weight()=0.17 ,distTo[w]=0.29 顶点w在pq队列中:w=3 v=3,进树---》顶点3开始 另一个顶点: 6 1 2 5 遍历顶点v的邻接边:v=3,e=3-6 0.52000 w=6 遍历顶点v的邻接边:v=3,e=1-3 0.29000 w=1 已进树,跳过w=1 遍历顶点v的邻接边:v=3,e=2-3 0.17000 w=2 已进树,跳过w=2 v=5,进树---》顶点5开始 另一个顶点: 1 7 4 w=1 已进树,跳过w=1 遍历顶点v的邻接边:v=5,e=5-7 0.28000 w=7 已进树,跳过w=7 遍历顶点v的邻接边:v=5,e=4-5 0.35000 w=4 e.weight()=0.35 ,distTo[w]=0.37 顶点w在pq队列中:w=4 v=4,进树---》顶点4开始 另一个顶点:6 0 7 5 遍历顶点v的邻接边:v=4,e=6-4 0.93000 w=6 遍历顶点v的邻接边:v=4,e=0-4 0.38000 w=0 已进树,跳过w=0 遍历顶点v的邻接边:v=4,e=4-7 0.37000 w=7 已进树,跳过w=7 遍历顶点v的邻接边:v=4,e=4-5 0.35000 w=5 已进树,跳过w=5 v=6,进树---》顶点6开始 另一个顶点:4 0 3 2 遍历顶点v的邻接边:v=6,e=6-4 0.93000 w=4 已进树,跳过w=4 遍历顶点v的邻接边:v=6,e=6-0 0.58000 w=0 已进树,跳过w=0 遍历顶点v的邻接边:v=6,e=3-6 0.52000 w=3 已进树,跳过w=3 遍历顶点v的邻接边:v=6,e=6-2 0.40000 w=2 已进树,跳过w=2----从顶点0作为原点,构建最小生成树,---end! v=1,marked[v]=true----从顶点1作为原点,构建最小生成树,已经入树,跳过!后续节点都已进树,都跳过。 v=2,marked[v]=true v=3,marked[v]=true v=4,marked[v]=true v=5,marked[v]=true v=6,marked[v]=true v=7,marked[v]=true 1-7 0.19000---》打印最小生成树的边 权重 0-2 0.26000 2-3 0.17000 4-5 0.35000 5-7 0.28000 6-2 0.40000 0-7 0.16000 1.810000---》打印最小生成树的总权重
三、总结
使用二叉堆(索引小值优先队列实现)优化的Prim算法,
3.1 空间复杂度
构造了几个顶点v索引的数组,所以和顶点数v成正比。
3.2 时间复杂度
主要就在优先队列的操作上
- 1.PrimMST->prim中:共v个顶点,每次取出最小值pq.delMin() 取第一个最小值元素,并把最后一个元素填充,新上来的这个元素下沉sink()大值下沉,时间复杂度=logv, 共v个顶点,也就是vlogv。
- 2.PrimMST->prim->scan 中 :遍历顶点的邻接表(邻接边链表),不管是 更新更小值 decreaseKey()、还是 插入新值 insert() ->都要执行swim()小值上浮, 时间复杂度=logv,一共E条边,所以Elogv.
所以:O(vlgv+elgv)=ElgV
3.3. 优化
1.如果使用斐波那契堆也就是Fredman-Tarjan算法,稠密图的decreaseKey操作耗时可以降到O(1),所以总耗时=O(V*logV+E*O(1))=ElgV
2.接近线性的算法Chazelle,但算法很复杂,就不再描述。
下一节,我们分析Kruskal算法。