From bd7b0e2e61af634f35b7544cb7654227ec6f37e1 Mon Sep 17 00:00:00 2001 From: Saurav Bhattacharya Date: Thu, 19 Mar 2026 11:15:07 -0700 Subject: [PATCH] refactor: replace List map keys with ArcKey in NetworkFlowAnalyzer Co-Authored-By: Claude Opus 4.6 (1M context) --- Gvisual/src/gvisual/NetworkFlowAnalyzer.java | 109 ++++++++++++------- 1 file changed, 67 insertions(+), 42 deletions(-) diff --git a/Gvisual/src/gvisual/NetworkFlowAnalyzer.java b/Gvisual/src/gvisual/NetworkFlowAnalyzer.java index 87a636b..0b0b13f 100644 --- a/Gvisual/src/gvisual/NetworkFlowAnalyzer.java +++ b/Gvisual/src/gvisual/NetworkFlowAnalyzer.java @@ -40,16 +40,16 @@ public class NetworkFlowAnalyzer { private final Graph graph; - // Residual capacities: directedKey -> remaining capacity - private Map, Double> residualCapacity; - // Flow values: directedKey -> flow - private Map, Double> flow; + // Residual capacities: ArcKey -> remaining capacity + private Map residualCapacity; + // Flow values: ArcKey -> flow + private Map flow; // Adjacency list for residual graph private Map> residualAdj; // Original capacities - private Map, Double> capacity; - // Edge lookup: directedKey -> original edge (null for reverse arcs) - private Map, edge> edgeLookup; + private Map capacity; + // Edge lookup: ArcKey -> original edge (null for reverse arcs) + private Map edgeLookup; private String source; private String sink; @@ -71,6 +71,41 @@ public NetworkFlowAnalyzer(Graph graph) { this.computed = false; } + // ── ArcKey ───────────────────────────────────────────────────── + + /** + * Immutable key for a directed arc between two vertices. + * Replaces the previous {@code List} map-key pattern with a + * type-safe, allocation-light alternative. + */ + static final class ArcKey { + final String from; + final String to; + + ArcKey(String from, String to) { + this.from = from; + this.to = to; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof ArcKey)) return false; + ArcKey that = (ArcKey) o; + return from.equals(that.from) && to.equals(that.to); + } + + @Override + public int hashCode() { + return 31 * from.hashCode() + to.hashCode(); + } + + @Override + public String toString() { + return from + "->" + to; + } + } + // ── Core computation ─────────────────────────────────────────── /** @@ -100,7 +135,7 @@ public double compute(String source, String sink) { // Edmonds–Karp: BFS for shortest augmenting path while (true) { Map parent = new LinkedHashMap(); - Map> parentArcKey = new LinkedHashMap>(); + Map parentArcKey = new LinkedHashMap(); double pathFlow = bfsAugmentingPath(parent, parentArcKey); if (pathFlow <= 0) break; @@ -109,8 +144,8 @@ public double compute(String source, String sink) { String v = sink; while (!v.equals(source)) { String u = parent.get(v); - List fwd = directedKey(u, v); - List rev = directedKey(v, u); + ArcKey fwd = directedKey(u, v); + ArcKey rev = directedKey(v, u); residualCapacity.put(fwd, residualCapacity.get(fwd) - pathFlow); @@ -173,17 +208,17 @@ public Map getEdgeFlows() { ensureComputed(); Map result = new LinkedHashMap(); for (edge e : graph.getEdges()) { - List fwd = directedKey(e.getVertex1(), e.getVertex2()); - List rev = directedKey(e.getVertex2(), e.getVertex1()); + ArcKey fwd = directedKey(e.getVertex1(), e.getVertex2()); + ArcKey rev = directedKey(e.getVertex2(), e.getVertex1()); double fwdFlow = flow.getOrDefault(fwd, 0.0); double revFlow = flow.getOrDefault(rev, 0.0); // Net flow direction if (fwdFlow > 1e-9) { - result.put(formatKey(fwd), fwdFlow); + result.put(fwd.toString(), fwdFlow); } else if (revFlow > 1e-9) { - result.put(formatKey(rev), revFlow); + result.put(rev.toString(), revFlow); } } return Collections.unmodifiableMap(result); @@ -346,7 +381,7 @@ public List decomposeFlowPaths() { ensureComputed(); // Work on a copy of flows - Map, Double> flowCopy = new HashMap, Double>(flow); + Map flowCopy = new HashMap(flow); List paths = new ArrayList(); while (true) { @@ -362,7 +397,7 @@ public List decomposeFlowPaths() { Set neighbors = residualAdj.get(u); if (neighbors == null) continue; for (String v : neighbors) { - List key = directedKey(u, v); + ArcKey key = directedKey(u, v); if (!parent.containsKey(v) && flowCopy.getOrDefault(key, 0.0) > 1e-9) { parent.put(v, u); @@ -394,7 +429,7 @@ public List decomposeFlowPaths() { // Subtract flow for (int i = 0; i < pathVertices.size() - 1; i++) { - List key = directedKey(pathVertices.get(i), pathVertices.get(i + 1)); + ArcKey key = directedKey(pathVertices.get(i), pathVertices.get(i + 1)); flowCopy.put(key, flowCopy.getOrDefault(key, 0.0) - pathFlow); } @@ -536,8 +571,8 @@ public String getSummary() { for (edge e : graph.getEdges()) { String v1 = e.getVertex1(); String v2 = e.getVertex2(); - List fwdKey = directedKey(v1, v2); - List revKey = directedKey(v2, v1); + ArcKey fwdKey = directedKey(v1, v2); + ArcKey revKey = directedKey(v2, v1); double fwdFlow = flow.getOrDefault(fwdKey, 0.0); double revFlow = flow.getOrDefault(revKey, 0.0); @@ -545,11 +580,11 @@ public String getSummary() { if (fwdFlow > 1e-9) { sb.append(String.format(" %s: %.2f / %.2f%s\n", - formatKey(fwdKey), fwdFlow, cap, + fwdKey, fwdFlow, cap, Math.abs(fwdFlow - cap) < 1e-9 ? " [SATURATED]" : "")); } else if (revFlow > 1e-9) { sb.append(String.format(" %s: %.2f / %.2f%s\n", - formatKey(revKey), revFlow, cap, + revKey, revFlow, cap, Math.abs(revFlow - cap) < 1e-9 ? " [SATURATED]" : "")); } } @@ -561,11 +596,11 @@ public String getSummary() { // ── Internal helpers ─────────────────────────────────────────── private void buildResidualGraph() { - residualCapacity = new HashMap, Double>(); - flow = new HashMap, Double>(); + residualCapacity = new HashMap(); + flow = new HashMap(); residualAdj = new HashMap>(); - capacity = new HashMap, Double>(); - edgeLookup = new HashMap, edge>(); + capacity = new HashMap(); + edgeLookup = new HashMap(); // Initialise adjacency sets for all vertices for (String v : graph.getVertices()) { @@ -577,8 +612,8 @@ private void buildResidualGraph() { String v2 = e.getVertex2(); double cap = getEdgeCapacity(e); - List fwd = directedKey(v1, v2); - List rev = directedKey(v2, v1); + ArcKey fwd = directedKey(v1, v2); + ArcKey rev = directedKey(v2, v1); // Each undirected edge → two directed arcs residualCapacity.put(fwd, @@ -598,7 +633,7 @@ private void buildResidualGraph() { } private double bfsAugmentingPath(Map parent, - Map> parentArcKey) { + Map parentArcKey) { Queue queue = new LinkedList(); queue.add(source); parent.put(source, null); @@ -609,7 +644,7 @@ private double bfsAugmentingPath(Map parent, if (neighbors == null) continue; for (String v : neighbors) { - List key = directedKey(u, v); + ArcKey key = directedKey(u, v); if (!parent.containsKey(v) && residualCapacity.getOrDefault(key, 0.0) > 1e-9) { parent.put(v, u); @@ -620,7 +655,7 @@ private double bfsAugmentingPath(Map parent, String t = sink; while (!t.equals(source)) { String p = parent.get(t); - List k = directedKey(p, t); + ArcKey k = directedKey(p, t); pathFlow = Math.min(pathFlow, residualCapacity.getOrDefault(k, 0.0)); t = p; @@ -639,18 +674,8 @@ private double getEdgeCapacity(edge e) { return w > 0 ? w : 1.0; } - private List directedKey(String from, String to) { - return Arrays.asList(from, to); - } - - /** - * Formats a directed key as a human-readable string for display purposes. - * - * @param key the directed key (two-element list) - * @return formatted string "from->to" - */ - private String formatKey(List key) { - return key.get(0) + "->" + key.get(1); + private ArcKey directedKey(String from, String to) { + return new ArcKey(from, to); } private void validateVertex(String vertex, String name) {