From 11219a0b92d5936045a87e0ce74c811a14e2ab05 Mon Sep 17 00:00:00 2001 From: William Fiset Date: Tue, 24 Mar 2026 09:21:56 -0700 Subject: [PATCH] Refactor FloydWarshallSolver: clean up, add docs, simplify examples Co-Authored-By: Claude Opus 4.6 --- .../graphtheory/FloydWarshallSolver.java | 186 ++++++++---------- 1 file changed, 79 insertions(+), 107 deletions(-) diff --git a/src/main/java/com/williamfiset/algorithms/graphtheory/FloydWarshallSolver.java b/src/main/java/com/williamfiset/algorithms/graphtheory/FloydWarshallSolver.java index 7bf794f18..73e87e550 100644 --- a/src/main/java/com/williamfiset/algorithms/graphtheory/FloydWarshallSolver.java +++ b/src/main/java/com/williamfiset/algorithms/graphtheory/FloydWarshallSolver.java @@ -1,26 +1,26 @@ /** - * This file contains an implementation of the Floyd-Warshall algorithm to find all pairs of - * shortest paths between nodes in a graph. We also demonstrate how to detect negative cycles and - * reconstruct the shortest path. + * Implementation of the Floyd-Warshall algorithm to find all pairs of shortest paths between nodes + * in a graph. Also demonstrates how to detect negative cycles and reconstruct the shortest path. * - *

Time Complexity: O(V^3) + *

Time: O(V^3) + * + *

Space: O(V^2) * * @author Micah Stairs, William Fiset */ package com.williamfiset.algorithms.graphtheory; -// Import Java's special constants ∞ and -∞ which behave -// as you expect them to when you do arithmetic. For example, -// ∞ + ∞ = ∞, ∞ + x = ∞, -∞ + x = -∞ and ∞ + -∞ = Nan import static java.lang.Double.NEGATIVE_INFINITY; import static java.lang.Double.POSITIVE_INFINITY; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import java.util.stream.Collectors; public class FloydWarshallSolver { - private int n; + private final int n; private boolean solved; private double[][] dp; private Integer[][] next; @@ -28,22 +28,27 @@ public class FloydWarshallSolver { private static final int REACHES_NEGATIVE_CYCLE = -1; /** - * As input, this class takes an adjacency matrix with edge weights between nodes, where - * POSITIVE_INFINITY is used to indicate that two nodes are not connected. + * Creates a Floyd-Warshall solver from an adjacency matrix with edge weights between nodes, where + * POSITIVE_INFINITY indicates that two nodes are not connected. * *

NOTE: Usually the diagonal of the adjacency matrix is all zeros (i.e. matrix[i][i] = 0 for - * all i) since there is typically no cost to go from a node to itself, but this may depend on - * your graph and the problem you are trying to solve. + * all i) since there is typically no cost to go from a node to itself, but this may depend on the + * graph and the problem being solved. + * + * @param matrix an n x n adjacency matrix of edge weights. + * @throws IllegalArgumentException if the matrix is null or empty. */ public FloydWarshallSolver(double[][] matrix) { + if (matrix == null || matrix.length == 0) + throw new IllegalArgumentException("Matrix cannot be null or empty."); n = matrix.length; dp = new double[n][n]; next = new Integer[n][n]; - // Copy input matrix and setup 'next' matrix for path reconstruction. for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { - if (matrix[i][j] != POSITIVE_INFINITY) next[i][j] = j; + if (matrix[i][j] != POSITIVE_INFINITY) + next[i][j] = j; dp[i][j] = matrix[i][j]; } } @@ -52,30 +57,28 @@ public FloydWarshallSolver(double[][] matrix) { /** * Runs Floyd-Warshall to compute the shortest distance between every pair of nodes. * - * @return The solved All Pairs Shortest Path (APSP) matrix. + * @return the solved All Pairs Shortest Path (APSP) matrix. */ public double[][] getApspMatrix() { solve(); return dp; } - // Executes the Floyd-Warshall algorithm. + /** Executes the Floyd-Warshall algorithm. */ public void solve() { - if (solved) return; + if (solved) + return; // Compute all pairs shortest paths. - for (int k = 0; k < n; k++) { - for (int i = 0; i < n; i++) { - for (int j = 0; j < n; j++) { + for (int k = 0; k < n; k++) + for (int i = 0; i < n; i++) + for (int j = 0; j < n; j++) if (dp[i][k] + dp[k][j] < dp[i][j]) { dp[i][j] = dp[i][k] + dp[k][j]; next[i][j] = next[i][k]; } - } - } - } - // Identify negative cycles by propagating the value 'NEGATIVE_INFINITY' + // Identify negative cycles by propagating NEGATIVE_INFINITY // to every edge that is part of or reaches into a negative cycle. for (int k = 0; k < n; k++) for (int i = 0; i < n; i++) @@ -91,117 +94,86 @@ public void solve() { /** * Reconstructs the shortest path (of nodes) from 'start' to 'end' inclusive. * - * @return An array of nodes indexes of the shortest path from 'start' to 'end'. If 'start' and - * 'end' are not connected return an empty array. If the shortest path from 'start' to 'end' - * are reachable by a negative cycle return -1. + * @return an array of node indexes of the shortest path from 'start' to 'end'. If 'start' and + * 'end' are not connected return an empty list. If the shortest path from 'start' to 'end' + * reaches a negative cycle return null. */ public List reconstructShortestPath(int start, int end) { solve(); List path = new ArrayList<>(); - if (dp[start][end] == POSITIVE_INFINITY) return path; + if (dp[start][end] == POSITIVE_INFINITY) + return path; int at = start; for (; at != end; at = next[at][end]) { - // Return null since there are an infinite number of shortest paths. - if (at == REACHES_NEGATIVE_CYCLE) return null; + if (at == REACHES_NEGATIVE_CYCLE) + return null; path.add(at); } - // Return null since there are an infinite number of shortest paths. - if (next[at][end] == REACHES_NEGATIVE_CYCLE) return null; + if (next[at][end] == REACHES_NEGATIVE_CYCLE) + return null; path.add(end); return path; } - /* Example usage. */ - - // Creates a graph with n nodes. The adjacency matrix is constructed - // such that the value of going from a node to itself is 0. + /** Creates an n x n adjacency matrix initialized with POSITIVE_INFINITY and zero diagonal. */ public static double[][] createGraph(int n) { double[][] matrix = new double[n][n]; for (int i = 0; i < n; i++) { - java.util.Arrays.fill(matrix[i], POSITIVE_INFINITY); + Arrays.fill(matrix[i], POSITIVE_INFINITY); matrix[i][i] = 0; } return matrix; } public static void main(String[] args) { - // Construct graph. - int n = 7; - double[][] m = createGraph(n); + exampleWithNegativeCycle(); + System.out.println(); + exampleSimpleGraph(); + } - // Add some edge values. - m[0][1] = 2; - m[0][2] = 5; - m[0][6] = 10; - m[1][2] = 2; - m[1][4] = 11; - m[2][6] = 2; - m[6][5] = 11; - m[4][5] = 1; - m[5][4] = -2; + // Example 1: 4-node graph with a negative cycle between nodes 2 and 3. + private static void exampleWithNegativeCycle() { + int n = 4; + double[][] m = createGraph(n); + m[0][1] = 4; + m[1][2] = 1; + m[2][3] = 2; + m[3][2] = -5; // Creates negative cycle: 2 -> 3 -> 2 (net cost -3). FloydWarshallSolver solver = new FloydWarshallSolver(m); double[][] dist = solver.getApspMatrix(); - for (int i = 0; i < n; i++) - for (int j = 0; j < n; j++) - System.out.printf("This shortest path from node %d to node %d is %.3f\n", i, j, dist[i][j]); - - // Prints: - // This shortest path from node 0 to node 0 is 0.000 - // This shortest path from node 0 to node 1 is 2.000 - // This shortest path from node 0 to node 2 is 4.000 - // This shortest path from node 0 to node 3 is Infinity - // This shortest path from node 0 to node 4 is -Infinity - // This shortest path from node 0 to node 5 is -Infinity - // This shortest path from node 0 to node 6 is 6.000 - // This shortest path from node 1 to node 0 is Infinity - // This shortest path from node 1 to node 1 is 0.000 - // This shortest path from node 1 to node 2 is 2.000 - // This shortest path from node 1 to node 3 is Infinity - // ... + System.out.println("=== Example 1: Negative cycle ==="); + System.out.printf("dist(0, 1) = %.0f\n", dist[0][1]); // 4 + System.out.printf("dist(0, 2) = %.0f\n", dist[0][2]); // -Infinity (reaches negative cycle) + System.out.printf("path(0, 2) = %s\n", formatPath(solver.reconstructShortestPath(0, 2), 0, 2)); + System.out.printf("path(0, 1) = %s\n", formatPath(solver.reconstructShortestPath(0, 1), 0, 1)); + } - System.out.println(); + // Example 2: 4-node directed graph with no negative cycles. + private static void exampleSimpleGraph() { + int n = 4; + double[][] m = createGraph(n); + m[0][1] = 1; + m[1][2] = 3; + m[1][3] = 10; + m[2][3] = 2; - // Reconstructs the shortest paths from all nodes to every other nodes. - for (int i = 0; i < n; i++) { - for (int j = 0; j < n; j++) { - List path = solver.reconstructShortestPath(i, j); - String str; - if (path == null) { - str = "HAS AN ∞ NUMBER OF SOLUTIONS! (negative cycle case)"; - } else if (path.size() == 0) { - str = String.format("DOES NOT EXIST (node %d doesn't reach node %d)", i, j); - } else { - str = - String.join( - " -> ", - path.stream() - .map(Object::toString) - .collect(java.util.stream.Collectors.toList())); - str = "is: [" + str + "]"; - } - - System.out.printf("The shortest path from node %d to node %d %s\n", i, j, str); - } - } + FloydWarshallSolver solver = new FloydWarshallSolver(m); + double[][] dist = solver.getApspMatrix(); + + System.out.println("=== Example 2: Simple directed graph ==="); + // Shortest distance from 0 to 3 is 6 (0 -> 1 -> 2 -> 3), not 11 (0 -> 1 -> 3). + System.out.printf("dist(0, 3) = %.0f\n", dist[0][3]); + System.out.printf("path(0, 3) = %s\n", formatPath(solver.reconstructShortestPath(0, 3), 0, 3)); + System.out.printf("path(3, 0) = %s\n", formatPath(solver.reconstructShortestPath(3, 0), 3, 0)); + } - // Prints: - // The shortest path from node 0 to node 0 is: [0] - // The shortest path from node 0 to node 1 is: [0 -> 1] - // The shortest path from node 0 to node 2 is: [0 -> 1 -> 2] - // The shortest path from node 0 to node 3 DOES NOT EXIST (node 0 doesn't reach node 3) - // The shortest path from node 0 to node 4 HAS AN ∞ NUMBER OF SOLUTIONS! (negative cycle case) - // The shortest path from node 0 to node 5 HAS AN ∞ NUMBER OF SOLUTIONS! (negative cycle case) - // The shortest path from node 0 to node 6 is: [0 -> 1 -> 2 -> 6] - // The shortest path from node 1 to node 0 DOES NOT EXIST (node 1 doesn't reach node 0) - // The shortest path from node 1 to node 1 is: [1] - // The shortest path from node 1 to node 2 is: [1 -> 2] - // The shortest path from node 1 to node 3 DOES NOT EXIST (node 1 doesn't reach node 3) - // The shortest path from node 1 to node 4 HAS AN ∞ NUMBER OF SOLUTIONS! (negative cycle case) - // The shortest path from node 1 to node 5 HAS AN ∞ NUMBER OF SOLUTIONS! (negative cycle case) - // The shortest path from node 1 to node 6 is: [1 -> 2 -> 6] - // The shortest path from node 2 to node 0 DOES NOT EXIST (node 2 doesn't reach node 0) - // ... + private static String formatPath(List path, int start, int end) { + if (path == null) + return "NEGATIVE CYCLE"; + if (path.isEmpty()) + return String.format("NO PATH (%d doesn't reach %d)", start, end); + return path.stream().map(Object::toString).collect(Collectors.joining(" -> ")); } }