Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions DIRECTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
150 changes: 150 additions & 0 deletions src/main/java/com/thealgorithms/graph/HungarianAlgorithm.java
Original file line number Diff line number Diff line change
@@ -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");
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}