mirror of
https://github.com/TheAlgorithms/Java.git
synced 2025-12-19 07:00:35 +08:00
feat: Add Hungarian Algorithm (Assignment Problem) with tests and reference [GRAPHS] (#6808)
* feat(graph): add Hungarian Algorithm (assignment) with tests; update directory * style(graph): split multiple variable declarations to satisfy Checkstyle
This commit is contained in:
@@ -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)
|
||||
|
||||
150
src/main/java/com/thealgorithms/graph/HungarianAlgorithm.java
Normal file
150
src/main/java/com/thealgorithms/graph/HungarianAlgorithm.java
Normal file
@@ -0,0 +1,150 @@
|
||||
package com.thealgorithms.graph;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Hungarian algorithm (a.k.a. Kuhn–Munkres) for the Assignment Problem.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>Time complexity: O(n^3) with n = max(rows, cols).
|
||||
*
|
||||
* <p>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 <a href="https://en.wikipedia.org/wiki/Hungarian_algorithm">Wikipedia: Hungarian algorithm</a>
|
||||
*/
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user