▶ 书中第六章部分程序,包括在加上自己补充的代码,包括全局最小切分 Stoer-Wagner 算法,二分图最大匹配(最小顶点覆盖)的交替路径算法和 HopcroftKarp 算法
● 全局最小切分 Stoer-Wagner 算法
1 package package01; 2 3 import edu.princeton.cs.algs4.In; 4 import edu.princeton.cs.algs4.StdOut; 5 import edu.princeton.cs.algs4.EdgeWeightedGraph; 6 import edu.princeton.cs.algs4.Edge; 7 import edu.princeton.cs.algs4.UF; 8 import edu.princeton.cs.algs4.IndexMaxPQ; 9 10 public class class01 11 { 12 private static final double FLOATING_POINT_EPSILON = 1E-11; 13 private double weight = Double.POSITIVE_INFINITY; // 输出的最小割值 14 private boolean[] cut; // 顶点是否在割除集 T 中 15 private int V; 16 17 private class CutPhase // 最小 s-t 割类(cut-of-the-phase) 18 { 19 private double weight; 20 private int s; 21 private int t; 22 23 public CutPhase(double inputWeight, int inputS, int inputT) 24 { 25 weight = inputWeight; 26 s = inputS; 27 t = inputT; 28 } 29 } 30 31 public class01(EdgeWeightedGraph G) 32 { 33 UF uf = new UF(G.V()); // 用类 union–find 来表示顶点的合并情况 34 boolean[] marked = new boolean[G.V()]; // 已合并的顶点集,初始化为空 35 cut = new boolean[G.V()]; // 割除集 T,初始化为空 36 CutPhase cp = new CutPhase(0.0, 0, 0); // 用于首次搜索的割,无意义 37 for (int v = G.V(); --v > 0; marked[cp.t] = true) // 遍历 V-1 次,每次标记被合并的顶点,以后不再遍历该点 38 { 39 cp = minCutPhase(G, marked, cp); // 更新最小割 40 if (cp.weight < weight) // 发现权值更小的割,更新全局割 41 { 42 weight = cp.weight; 43 for (int j = 0; j < G.V(); j++) // 在最新的图中,与 cp.t 相连的顶点都是 T 的元素 44 cut[j] = uf.connected(j, cp.t); 45 } 46 G = contractEdge(G, cp.s, cp.t); // 顶点 t 合并到顶点 s,更新图 47 uf.union(cp.s, cp.t); // 顶点 t 加入 T 中 48 } 49 } 50 51 public double weight() 52 { 53 return weight; 54 } 55 56 public boolean cut(int v) 57 { 58 return cut[v]; 59 } 60 61 private CutPhase minCutPhase(EdgeWeightedGraph G, boolean[] marked, CutPhase cp) // 计算 s - t 的最小割,称为 maximum adjacency (cardinality) search 62 { 63 IndexMaxPQ<Double> pq = new IndexMaxPQ<Double>(G.V()); // 用于挑选权值最大的点用于合并 64 pq.insert(cp.s, Double.POSITIVE_INFINITY); // 顶点 s 自己的权值为 +∞ 65 for (int v = 0; v < G.V(); v++) // 其他顶点权值初始化为 0 66 { 67 if (v != cp.s && !marked[v]) 68 pq.insert(v, 0.0); 69 } 70 for (; !pq.isEmpty();) 71 { 72 cp.s = cp.t; // 记录最后取出的两个顶点,每次往前挪一格 73 cp.t = pq.delMax(); // 取走权值最大的顶点 v 74 for (Edge e : G.adj(cp.t)) // 只要 cp.t 的邻居还在 pq 中没有被取走,就更新邻居的权值 75 { 76 int w = e.other(cp.t); 77 if (pq.contains(w)) 78 pq.increaseKey(w, pq.keyOf(w) + e.weight()); // 顶点 w 的权值自增边 e 的权值 79 } 80 } 81 cp.weight = 0.0; // 计算最后加入 T 的顶点的权值,即为最小割值 82 for (Edge e : G.adj(cp.t)) 83 cp.weight += e.weight(); 84 return cp; 85 } 86 87 private EdgeWeightedGraph contractEdge(EdgeWeightedGraph G, int s, int t) // 把顶点 t 合并到顶点 s,更新其他边的权值 88 { 89 EdgeWeightedGraph H = new EdgeWeightedGraph(G.V()); // 合并后的图,顶点与原来相同 90 for (int v = 0; v < G.V(); v++) 91 { 92 for (Edge e : G.adj(v)) // 依顶点序遍历所有边 93 { 94 int w = e.other(v); 95 if (v == s && w == t || v == t && w == s) // 边 s-t 自身不要 96 continue; 97 if (v < w) // 只考虑后向边,滤掉重复 98 { 99 if (w == t) // 远端顶点 w 是被合并的 t,边 v - w(t) 替换成边 v - s 100 H.addEdge(new Edge(v, s, e.weight())); 101 else if (v == t) // 近端顶点 v 是被合并的 t,边 v(t) - w 替换成边 s - w 102 H.addEdge(new Edge(w, s, e.weight())); 103 else // 边 v - w 与 s 或 t 无关,原样放进 H 104 H.addEdge(new Edge(v, w, e.weight())); 105 } 106 } 107 } 108 return H; 109 } 110 111 public static void main(String[] args) 112 { 113 In in = new In(args[0]); 114 EdgeWeightedGraph G = new EdgeWeightedGraph(in); 115 class01 mc = new class01(G); 116 StdOut.print("Min cut: "); 117 for (int v = 0; v < G.V(); v++) 118 { 119 if (mc.cut(v)) 120 StdOut.print(v + " "); 121 } 122 StdOut.println("\nMin cut weight = " + mc.weight()); 123 } 124 }
● 二分图最大匹配(最小顶点覆盖)的交替路径算法
1 package package01; 2 3 import edu.princeton.cs.algs4.StdOut; 4 import edu.princeton.cs.algs4.BipartiteX; 5 import edu.princeton.cs.algs4.Graph; 6 import edu.princeton.cs.algs4.GraphGenerator; 7 import edu.princeton.cs.algs4.Queue; 8 9 public class class01 10 { 11 private final int V; 12 private BipartiteX bipartition; 13 private int cardinality; // 匹配集的顶点数 14 private int[] mate; // 每个顶点的配对顶点,-1 表示未配对 15 private boolean[] inMinVertexCover; // 顶点是否处于最小覆盖中 16 private boolean[] marked; // 标记已经搜索过的顶点 17 private int[] edgeTo; // 搜索序列中的父节点标号 18 19 public class01(Graph G) 20 { 21 V = G.V(); 22 mate = new int[V]; 23 for (int v = 0; v < V; v++) 24 mate[v] = -1; 25 for (; hasAugmentingPath(G);) 26 { 27 int t = -1; 28 for (int v = 0; v < G.V(); v++) // 寻找增广路径的一个端点,它不在 mate 表中,但在搜索序列中 29 { 30 if (!isMatched(v) && edgeTo[v] != -1) 31 { 32 t = v; 33 break; 34 } 35 } 36 for (int v = t; v != -1; v = edgeTo[edgeTo[v]]) // 增广路经中每两个相邻顶点做成匹配 37 { 38 int w = edgeTo[v]; 39 mate[v] = w; 40 mate[w] = v; 41 } 42 cardinality++; // 全部调整完成,匹配集中顶点数量增加 1 43 } 44 inMinVertexCover = new boolean[V]; // 更新 inMinVertexCover[],加入最小覆盖的条件是未匹配的黑点和已匹配的红点 45 for (int v = 0; v < V; v++) 46 { 47 if (bipartition.color(v) && !marked[v] || !bipartition.color(v) && marked[v]) 48 inMinVertexCover[v] = true; 49 } 50 assert certifySolution(G); 51 } 52 53 private boolean hasAugmentingPath(Graph G) // 是否存在可调整路径,算法是寻找图中最短增广路经,注意函数对 mate[] 只读 54 // 交替路径:沿着交替路径前进,每条边依次属于、不属于匹配集合 55 // 增广路经:起点和终点都属于未匹配集的交替路径,说明可以通过对换路径上所有边(匹配 <-> 未匹配)来增加匹配集的顶点数 56 { 57 marked = new boolean[V]; 58 edgeTo = new int[V]; 59 for (int v = 0; v < V; v++) 60 edgeTo[v] = -1; 61 Queue<Integer> queue = new Queue<Integer>(); 62 for (int v = 0; v < V; v++) 63 { 64 if (bipartition.color(v) && !isMatched(v)) // 所有未配对的黑色点(true)加入搜索队列,作为初始点 65 { 66 queue.enqueue(v); 67 marked[v] = true; 68 } 69 } 70 for (; !queue.isEmpty();) 71 { 72 int v = queue.dequeue(); 73 for (int w : G.adj(v)) 74 { 75 if (isResidualGraphEdge(v, w) && !marked[w])// w 是一个新顶点,v - w 是未配对黑-红边且或是已配对红-黑边 76 { 77 edgeTo[w] = v; // 搜索序列中,把 w 当做 v 的后继 78 marked[w] = true; 79 if (!isMatched(w)) // w 是未经配对的顶点,说明找到了增广路经,否则 w 也加入搜索队列 80 return true; 81 queue.enqueue(w); 82 } 83 } 84 } 85 return false; 86 } 87 88 private boolean isResidualGraphEdge(int v, int w) // 要么 v 黑且 v - w 未配对,要么 v 红且 v - w 已配对 89 { 90 return (mate[v] != w) && bipartition.color(v) || (mate[v] == w) && !bipartition.color(v); 91 } 92 93 public int mate(int v) 94 { 95 return mate[v]; 96 } 97 98 public boolean isMatched(int v) // mate[v] >= 0 返回 1,mate[v] == -1 返回 0 99 { 100 return mate[v] != -1; 101 } 102 103 public int size() 104 { 105 return cardinality; 106 } 107 108 public boolean isPerfect() // 是否为完美匹配 109 { 110 return cardinality * 2 == V; 111 } 112 113 public boolean inMinVertexCover(int v) // 顶点是否在最小覆盖中 114 { 115 return inMinVertexCover[v]; 116 } 117 118 private boolean certifySolution(Graph G) // 检查结果正确性 119 { 120 for (int v = 0; v < V; v++) // 检查 mate[] 对称性 121 { 122 if (mate(v) == -1) 123 continue; 124 if (mate(mate(v)) != v) 125 return false; 126 } 127 int matchedVertices = 0; // 检查 cardinality 与 mate[] 一致性 128 for (int v = 0; v < V; v++) 129 { 130 if (mate(v) != -1) 131 matchedVertices++; 132 } 133 if (2 * size() != matchedVertices) 134 return false; 135 int sizeOfMinVertexCover = 0; // 检查 cardinality 与 inMinVertexCover[] 一致性 136 for (int v = 0; v < V; v++) 137 { 138 if (inMinVertexCover(v)) 139 sizeOfMinVertexCover++; 140 } 141 if (size() != sizeOfMinVertexCover) 142 return false; 143 boolean[] isMatched = new boolean[V]; // 检查 mate[] 唯一性 144 for (int v = 0; v < V; v++) 145 { 146 int w = mate[v]; 147 if (w == -1) 148 continue; 149 if (v == w) 150 return false; 151 if (v >= w) 152 continue; 153 if (isMatched[v] || isMatched[w]) return false; 154 isMatched[v] = true; 155 isMatched[w] = true; 156 } 157 for (int v = 0; v < V; v++) // 检查匹配集中的每个顶点至少有一条远端也属于匹配集的边 158 { 159 if (mate(v) == -1) 160 continue; 161 boolean isEdge = false; 162 for (int w : G.adj(v)) 163 { 164 if (mate(v) == w) 165 isEdge = true; 166 } 167 if (!isEdge) 168 return false; 169 } 170 for (int v = 0; v < V; v++) // 检查 inMinVertexCover[] 是一个覆盖 171 { 172 for (int w : G.adj(v)) 173 { 174 if (!inMinVertexCover(v) && !inMinVertexCover(w)) 175 return false; 176 } 177 } 178 return true; 179 } 180 181 public static void main(String[] args) 182 { 183 int V1 = Integer.parseInt(args[0]); 184 int V2 = Integer.parseInt(args[1]); 185 int E = Integer.parseInt(args[2]); 186 Graph G = GraphGenerator.bipartite(V1, V2, E); 187 if (G.V() < 1000) 188 StdOut.println(G); 189 190 class01 matching = new class01(G); 191 192 StdOut.printf("Number of edges in max matching = %d\n", matching.size()); 193 StdOut.printf("Number of vertices in min vertex cover = %d\n", matching.size()); 194 StdOut.printf("Graph has a perfect matching = %b\n", matching.isPerfect()); 195 StdOut.println(); 196 197 if (G.V() >= 1000) 198 return; 199 StdOut.print("Max matching: "); 200 for (int v = 0; v < G.V(); v++) 201 { 202 int w = matching.mate(v); 203 if (matching.isMatched(v) && v < w) 204 StdOut.print(v + "-" + w + " "); 205 } 206 StdOut.print("\nMin vertex cover: "); 207 for (int v = 0; v < G.V(); v++) 208 { 209 if (matching.inMinVertexCover(v)) 210 StdOut.print(v + " "); 211 } 212 StdOut.println(); 213 } 214 }
● 二分图最大匹配(最小顶点覆盖)的 HopcroftKarp 算法,仅注释与普通交替路径法不同的地方
1 package package01; 2 3 import java.util.Iterator; 4 import edu.princeton.cs.algs4.StdOut; 5 import edu.princeton.cs.algs4.BipartiteX; 6 import edu.princeton.cs.algs4.Stack; 7 import edu.princeton.cs.algs4.Graph; 8 import edu.princeton.cs.algs4.GraphGenerator; 9 import edu.princeton.cs.algs4.Queue; 10 11 public class class01 12 { 13 private final int V; 14 private BipartiteX bipartition; 15 private int cardinality; 16 private int[] mate; 17 private boolean[] inMinVertexCover; 18 private boolean[] marked; 19 private int[] distTo; // 搜索序列中抵达每个顶点的最小路径长度 20 21 public class01(Graph G) 22 { 23 V = G.V(); 24 mate = new int[V]; 25 for (int v = 0; v < V; v++) 26 mate[v] = -1; 27 for (; hasAugmentingPath(G);) 28 { 29 Iterator<Integer>[] adj = (Iterator<Integer>[]) new Iterator[G.V()]; // 邻接表迭代器列表 30 for (int v = 0; v < G.V(); v++) 31 adj[v] = G.adj(v).iterator(); 32 for (int s = 0; s < V; s++) 33 { 34 if (isMatched(s) || !bipartition.color(s)) // 只取未配对的黑色(true)顶点 35 continue; 36 Stack<Integer> path = new Stack<Integer>(); // 使用非递归的深度优先遍历寻找关于顶点 s 的交替路径 37 for (path.push(s); !path.isEmpty();) 38 { 39 int v = path.peek(); 40 if (!adj[v].hasNext()) // 栈顶顶点没有出边,跳过 41 { 42 path.pop(); 43 continue; 44 } 45 int w = adj[v].next(); 46 if (!isLevelGraphEdge(v, w)) // 若 v -w 不是搜索队列生成的边,跳过 47 continue; 48 path.push(w); 49 if (!isMatched(w)) // w 是新点,它不在 mate 表中,但在搜索序列中 50 { 51 for (; !path.isEmpty();) // 增广路经中每两个相邻顶点做成匹配 52 { 53 int x = path.pop(), y = path.pop(); 54 mate[x] = y; 55 mate[y] = x; 56 } 57 cardinality++; 58 } 59 } 60 } 61 } 62 inMinVertexCover = new boolean[V]; 63 for (int v = 0; v < V; v++) 64 { 65 if (bipartition.color(v) && !marked[v] || !bipartition.color(v) && marked[v]) 66 inMinVertexCover[v] = true; 67 } 68 } 69 70 private boolean hasAugmentingPath(Graph G) 71 { 72 marked = new boolean[V]; 73 distTo = new int[V]; 74 for (int v = 0; v < V; v++) 75 distTo[v] = Integer.MAX_VALUE; 76 Queue<Integer> queue = new Queue<Integer>(); // 从未配对的顶点开始广度优先搜索 77 for (int v = 0; v < V; v++) 78 { 79 if (bipartition.color(v) && !isMatched(v)) // 所有未配对的黑色点(true)加入搜索队列,作为初始点,初始点距离为 0 80 { 81 queue.enqueue(v); 82 marked[v] = true; 83 distTo[v] = 0; 84 } 85 } 86 boolean hasAugmentingPath = false; 87 for (; !queue.isEmpty();) // 要运行直到所有顶点都被遍历过才结束 88 { 89 int v = queue.dequeue(); 90 for (int w : G.adj(v)) 91 { 92 if (isResidualGraphEdge(v, w) && !marked[w]) 93 { 94 distTo[w] = distTo[v] + 1; // 标记顶点距离为已知顶点加 1 而不标记父节点编号 95 marked[w] = true; 96 if (!isMatched(w)) 97 hasAugmentingPath = true; // 仅标记发现了交替路径而不返回 98 if (!hasAugmentingPath) 99 queue.enqueue(w); 100 } 101 } 102 } 103 return hasAugmentingPath; 104 } 105 106 private boolean isResidualGraphEdge(int v, int w) 107 { 108 return (mate[v] != w) && bipartition.color(v) || (mate[v] == w) && !bipartition.color(v); 109 } 110 111 private boolean isLevelGraphEdge(int v, int w) // v - w 是搜索队列生成的边 112 { 113 return (distTo[w] == distTo[v] + 1) && isResidualGraphEdge(v, w); 114 } 115 116 public int mate(int v) 117 { 118 return mate[v]; 119 } 120 121 public boolean isMatched(int v) 122 { 123 return mate[v] != -1; 124 } 125 126 public int size() 127 { 128 return cardinality; 129 } 130 131 public boolean isPerfect() 132 { 133 return cardinality * 2 == V; 134 } 135 136 public boolean inMinVertexCover(int v) 137 { 138 return inMinVertexCover[v]; 139 } 140 141 private static String toString(Iterable<Integer> path) 142 { 143 StringBuilder sb = new StringBuilder(); 144 for (int v : path) 145 sb.append(v + "-"); 146 String s = sb.toString(); 147 s = s.substring(0, s.lastIndexOf('-')); 148 return s; 149 } 150 151 public static void main(String[] args) 152 { 153 int V1 = Integer.parseInt(args[0]); 154 int V2 = Integer.parseInt(args[1]); 155 int E = Integer.parseInt(args[2]); 156 Graph G = GraphGenerator.bipartite(V1, V2, E); 157 if (G.V() < 1000) 158 StdOut.println(G); 159 160 class01 matching = new class01(G); 161 162 StdOut.printf("Number of edges in max matching = %d\n", matching.size()); 163 StdOut.printf("Number of vertices in min vertex cover = %d\n", matching.size()); 164 StdOut.printf("Graph has a perfect matching = %b\n", matching.isPerfect()); 165 StdOut.println(); 166 167 if (G.V() >= 1000) 168 return; 169 StdOut.print("Max matching: "); 170 for (int v = 0; v < G.V(); v++) 171 { 172 int w = matching.mate(v); 173 if (matching.isMatched(v) && v < w) 174 StdOut.print(v + "-" + w + " "); 175 } 176 StdOut.print("\nMin vertex cover: "); 177 for (int v = 0; v < G.V(); v++) 178 { 179 if (matching.inMinVertexCover(v)) 180 StdOut.print(v + " "); 181 } 182 StdOut.println(); 183 } 184 }