diff --git a/DIRECTORY.md b/DIRECTORY.md index 2d140d829cfd..47833a3f59f2 100644 --- a/DIRECTORY.md +++ b/DIRECTORY.md @@ -372,6 +372,7 @@ - πŸ“„ [Edmonds](src/main/java/com/thealgorithms/graph/Edmonds.java) - πŸ“„ [EdmondsKarp](src/main/java/com/thealgorithms/graph/EdmondsKarp.java) - πŸ“„ [HopcroftKarp](src/main/java/com/thealgorithms/graph/HopcroftKarp.java) + - πŸ“„ [HungarianAlgorithm](src/main/java/com/thealgorithms/graph/HungarianAlgorithm.java) - πŸ“„ [PredecessorConstrainedDfs](src/main/java/com/thealgorithms/graph/PredecessorConstrainedDfs.java) - πŸ“„ [PushRelabel](src/main/java/com/thealgorithms/graph/PushRelabel.java) - πŸ“„ [StronglyConnectedComponentOptimized](src/main/java/com/thealgorithms/graph/StronglyConnectedComponentOptimized.java) diff --git a/src/main/java/com/thealgorithms/graph/HungarianAlgorithm.java b/src/main/java/com/thealgorithms/graph/HungarianAlgorithm.java new file mode 100644 index 000000000000..84f5671c4f9d --- /dev/null +++ b/src/main/java/com/thealgorithms/graph/HungarianAlgorithm.java @@ -0,0 +1,150 @@ +package com.thealgorithms.graph; + +import java.util.Arrays; + +/** + * Hungarian algorithm (a.k.a. Kuhn–Munkres) for the Assignment Problem. + * + *

Given an n x m cost matrix (n tasks, m workers), finds a minimum-cost + * one-to-one assignment. If the matrix is rectangular, the algorithm pads to a + * square internally. Costs must be finite non-negative integers. + * + *

Time complexity: O(n^3) with n = max(rows, cols). + * + *

API returns the assignment as an array where {@code assignment[i]} is the + * column chosen for row i (or -1 if unassigned when rows != cols), and a total + * minimal cost. + * + * @see Wikipedia: Hungarian algorithm + */ +public final class HungarianAlgorithm { + + private HungarianAlgorithm() { + } + + /** Result holder for the Hungarian algorithm. */ + public static final class Result { + public final int[] assignment; // assignment[row] = col or -1 + public final int minCost; + + public Result(int[] assignment, int minCost) { + this.assignment = assignment; + this.minCost = minCost; + } + } + + /** + * Solves the assignment problem for a non-negative cost matrix. + * + * @param cost an r x c matrix of non-negative costs + * @return Result with row-to-column assignment and minimal total cost + * @throws IllegalArgumentException for null/empty or negative costs + */ + public static Result solve(int[][] cost) { + validate(cost); + int rows = cost.length; + int cols = cost[0].length; + int n = Math.max(rows, cols); + + // Build square matrix with padding 0 for missing cells + int[][] a = new int[n][n]; + for (int i = 0; i < n; i++) { + if (i < rows) { + for (int j = 0; j < n; j++) { + a[i][j] = (j < cols) ? cost[i][j] : 0; + } + } else { + Arrays.fill(a[i], 0); + } + } + + // Potentials and matching arrays + int[] u = new int[n + 1]; + int[] v = new int[n + 1]; + int[] p = new int[n + 1]; + int[] way = new int[n + 1]; + + for (int i = 1; i <= n; i++) { + p[0] = i; + int j0 = 0; + int[] minv = new int[n + 1]; + boolean[] used = new boolean[n + 1]; + Arrays.fill(minv, Integer.MAX_VALUE); + Arrays.fill(used, false); + do { + used[j0] = true; + int i0 = p[j0]; + int delta = Integer.MAX_VALUE; + int j1 = 0; + for (int j = 1; j <= n; j++) { + if (!used[j]) { + int cur = a[i0 - 1][j - 1] - u[i0] - v[j]; + if (cur < minv[j]) { + minv[j] = cur; + way[j] = j0; + } + if (minv[j] < delta) { + delta = minv[j]; + j1 = j; + } + } + } + for (int j = 0; j <= n; j++) { + if (used[j]) { + u[p[j]] += delta; + v[j] -= delta; + } else { + minv[j] -= delta; + } + } + j0 = j1; + } while (p[j0] != 0); + do { + int j1 = way[j0]; + p[j0] = p[j1]; + j0 = j1; + } while (j0 != 0); + } + + int[] matchColForRow = new int[n]; + Arrays.fill(matchColForRow, -1); + for (int j = 1; j <= n; j++) { + if (p[j] != 0) { + matchColForRow[p[j] - 1] = j - 1; + } + } + + // Build assignment for original rows only, ignore padded rows + int[] assignment = new int[rows]; + Arrays.fill(assignment, -1); + int total = 0; + for (int i = 0; i < rows; i++) { + int j = matchColForRow[i]; + if (j >= 0 && j < cols) { + assignment[i] = j; + total += cost[i][j]; + } + } + return new Result(assignment, total); + } + + private static void validate(int[][] cost) { + if (cost == null || cost.length == 0) { + throw new IllegalArgumentException("Cost matrix must not be null or empty"); + } + int c = cost[0].length; + if (c == 0) { + throw new IllegalArgumentException("Cost matrix must have at least 1 column"); + } + for (int i = 0; i < cost.length; i++) { + if (cost[i] == null || cost[i].length != c) { + throw new IllegalArgumentException("Cost matrix must be rectangular with equal row lengths"); + } + for (int j = 0; j < c; j++) { + if (cost[i][j] < 0) { + throw new IllegalArgumentException("Costs must be non-negative"); + } + } + } + } +} diff --git a/src/test/java/com/thealgorithms/graph/HungarianAlgorithmTest.java b/src/test/java/com/thealgorithms/graph/HungarianAlgorithmTest.java new file mode 100644 index 000000000000..1344934b62e4 --- /dev/null +++ b/src/test/java/com/thealgorithms/graph/HungarianAlgorithmTest.java @@ -0,0 +1,44 @@ +package com.thealgorithms.graph; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class HungarianAlgorithmTest { + + @Test + @DisplayName("Classic 3x3 example: minimal cost 5 with assignment [1,0,2]") + void classicSquareExample() { + int[][] cost = {{4, 1, 3}, {2, 0, 5}, {3, 2, 2}}; + HungarianAlgorithm.Result res = HungarianAlgorithm.solve(cost); + assertEquals(5, res.minCost); + assertArrayEquals(new int[] {1, 0, 2}, res.assignment); + } + + @Test + @DisplayName("Rectangular (more rows than cols): pads to square and returns -1 for unassigned rows") + void rectangularMoreRows() { + int[][] cost = {{7, 3}, {2, 8}, {5, 1}}; + // Optimal selects any 2 rows: choose row1->col0 (2) and row2->col1 (1) => total 3 + HungarianAlgorithm.Result res = HungarianAlgorithm.solve(cost); + assertEquals(3, res.minCost); + // Two rows assigned to 2 columns; one row remains -1. + int assigned = 0; + for (int a : res.assignment) { + if (a >= 0) { + assigned++; + } + } + assertEquals(2, assigned); + } + + @Test + @DisplayName("Zero diagonal yields zero total cost") + void zeroDiagonal() { + int[][] cost = {{0, 5, 9}, {4, 0, 7}, {3, 6, 0}}; + HungarianAlgorithm.Result res = HungarianAlgorithm.solve(cost); + assertEquals(0, res.minCost); + } +}