Skip to content

Commit 98e11da

Browse files
authored
Merge branch 'master' into DampedOscillator
2 parents dbf274e + fb5a765 commit 98e11da

File tree

3 files changed

+195
-0
lines changed

3 files changed

+195
-0
lines changed

DIRECTORY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,7 @@
372372
- 📄 [Edmonds](src/main/java/com/thealgorithms/graph/Edmonds.java)
373373
- 📄 [EdmondsKarp](src/main/java/com/thealgorithms/graph/EdmondsKarp.java)
374374
- 📄 [HopcroftKarp](src/main/java/com/thealgorithms/graph/HopcroftKarp.java)
375+
- 📄 [HungarianAlgorithm](src/main/java/com/thealgorithms/graph/HungarianAlgorithm.java)
375376
- 📄 [PredecessorConstrainedDfs](src/main/java/com/thealgorithms/graph/PredecessorConstrainedDfs.java)
376377
- 📄 [PushRelabel](src/main/java/com/thealgorithms/graph/PushRelabel.java)
377378
- 📄 [StronglyConnectedComponentOptimized](src/main/java/com/thealgorithms/graph/StronglyConnectedComponentOptimized.java)
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
package com.thealgorithms.graph;
2+
3+
import java.util.Arrays;
4+
5+
/**
6+
* Hungarian algorithm (a.k.a. Kuhn–Munkres) for the Assignment Problem.
7+
*
8+
* <p>Given an n x m cost matrix (n tasks, m workers), finds a minimum-cost
9+
* one-to-one assignment. If the matrix is rectangular, the algorithm pads to a
10+
* square internally. Costs must be finite non-negative integers.
11+
*
12+
* <p>Time complexity: O(n^3) with n = max(rows, cols).
13+
*
14+
* <p>API returns the assignment as an array where {@code assignment[i]} is the
15+
* column chosen for row i (or -1 if unassigned when rows != cols), and a total
16+
* minimal cost.
17+
*
18+
* @see <a href="https://en.wikipedia.org/wiki/Hungarian_algorithm">Wikipedia: Hungarian algorithm</a>
19+
*/
20+
public final class HungarianAlgorithm {
21+
22+
private HungarianAlgorithm() {
23+
}
24+
25+
/** Result holder for the Hungarian algorithm. */
26+
public static final class Result {
27+
public final int[] assignment; // assignment[row] = col or -1
28+
public final int minCost;
29+
30+
public Result(int[] assignment, int minCost) {
31+
this.assignment = assignment;
32+
this.minCost = minCost;
33+
}
34+
}
35+
36+
/**
37+
* Solves the assignment problem for a non-negative cost matrix.
38+
*
39+
* @param cost an r x c matrix of non-negative costs
40+
* @return Result with row-to-column assignment and minimal total cost
41+
* @throws IllegalArgumentException for null/empty or negative costs
42+
*/
43+
public static Result solve(int[][] cost) {
44+
validate(cost);
45+
int rows = cost.length;
46+
int cols = cost[0].length;
47+
int n = Math.max(rows, cols);
48+
49+
// Build square matrix with padding 0 for missing cells
50+
int[][] a = new int[n][n];
51+
for (int i = 0; i < n; i++) {
52+
if (i < rows) {
53+
for (int j = 0; j < n; j++) {
54+
a[i][j] = (j < cols) ? cost[i][j] : 0;
55+
}
56+
} else {
57+
Arrays.fill(a[i], 0);
58+
}
59+
}
60+
61+
// Potentials and matching arrays
62+
int[] u = new int[n + 1];
63+
int[] v = new int[n + 1];
64+
int[] p = new int[n + 1];
65+
int[] way = new int[n + 1];
66+
67+
for (int i = 1; i <= n; i++) {
68+
p[0] = i;
69+
int j0 = 0;
70+
int[] minv = new int[n + 1];
71+
boolean[] used = new boolean[n + 1];
72+
Arrays.fill(minv, Integer.MAX_VALUE);
73+
Arrays.fill(used, false);
74+
do {
75+
used[j0] = true;
76+
int i0 = p[j0];
77+
int delta = Integer.MAX_VALUE;
78+
int j1 = 0;
79+
for (int j = 1; j <= n; j++) {
80+
if (!used[j]) {
81+
int cur = a[i0 - 1][j - 1] - u[i0] - v[j];
82+
if (cur < minv[j]) {
83+
minv[j] = cur;
84+
way[j] = j0;
85+
}
86+
if (minv[j] < delta) {
87+
delta = minv[j];
88+
j1 = j;
89+
}
90+
}
91+
}
92+
for (int j = 0; j <= n; j++) {
93+
if (used[j]) {
94+
u[p[j]] += delta;
95+
v[j] -= delta;
96+
} else {
97+
minv[j] -= delta;
98+
}
99+
}
100+
j0 = j1;
101+
} while (p[j0] != 0);
102+
do {
103+
int j1 = way[j0];
104+
p[j0] = p[j1];
105+
j0 = j1;
106+
} while (j0 != 0);
107+
}
108+
109+
int[] matchColForRow = new int[n];
110+
Arrays.fill(matchColForRow, -1);
111+
for (int j = 1; j <= n; j++) {
112+
if (p[j] != 0) {
113+
matchColForRow[p[j] - 1] = j - 1;
114+
}
115+
}
116+
117+
// Build assignment for original rows only, ignore padded rows
118+
int[] assignment = new int[rows];
119+
Arrays.fill(assignment, -1);
120+
int total = 0;
121+
for (int i = 0; i < rows; i++) {
122+
int j = matchColForRow[i];
123+
if (j >= 0 && j < cols) {
124+
assignment[i] = j;
125+
total += cost[i][j];
126+
}
127+
}
128+
return new Result(assignment, total);
129+
}
130+
131+
private static void validate(int[][] cost) {
132+
if (cost == null || cost.length == 0) {
133+
throw new IllegalArgumentException("Cost matrix must not be null or empty");
134+
}
135+
int c = cost[0].length;
136+
if (c == 0) {
137+
throw new IllegalArgumentException("Cost matrix must have at least 1 column");
138+
}
139+
for (int i = 0; i < cost.length; i++) {
140+
if (cost[i] == null || cost[i].length != c) {
141+
throw new IllegalArgumentException("Cost matrix must be rectangular with equal row lengths");
142+
}
143+
for (int j = 0; j < c; j++) {
144+
if (cost[i][j] < 0) {
145+
throw new IllegalArgumentException("Costs must be non-negative");
146+
}
147+
}
148+
}
149+
}
150+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package com.thealgorithms.graph;
2+
3+
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
4+
import static org.junit.jupiter.api.Assertions.assertEquals;
5+
6+
import org.junit.jupiter.api.DisplayName;
7+
import org.junit.jupiter.api.Test;
8+
9+
class HungarianAlgorithmTest {
10+
11+
@Test
12+
@DisplayName("Classic 3x3 example: minimal cost 5 with assignment [1,0,2]")
13+
void classicSquareExample() {
14+
int[][] cost = {{4, 1, 3}, {2, 0, 5}, {3, 2, 2}};
15+
HungarianAlgorithm.Result res = HungarianAlgorithm.solve(cost);
16+
assertEquals(5, res.minCost);
17+
assertArrayEquals(new int[] {1, 0, 2}, res.assignment);
18+
}
19+
20+
@Test
21+
@DisplayName("Rectangular (more rows than cols): pads to square and returns -1 for unassigned rows")
22+
void rectangularMoreRows() {
23+
int[][] cost = {{7, 3}, {2, 8}, {5, 1}};
24+
// Optimal selects any 2 rows: choose row1->col0 (2) and row2->col1 (1) => total 3
25+
HungarianAlgorithm.Result res = HungarianAlgorithm.solve(cost);
26+
assertEquals(3, res.minCost);
27+
// Two rows assigned to 2 columns; one row remains -1.
28+
int assigned = 0;
29+
for (int a : res.assignment) {
30+
if (a >= 0) {
31+
assigned++;
32+
}
33+
}
34+
assertEquals(2, assigned);
35+
}
36+
37+
@Test
38+
@DisplayName("Zero diagonal yields zero total cost")
39+
void zeroDiagonal() {
40+
int[][] cost = {{0, 5, 9}, {4, 0, 7}, {3, 6, 0}};
41+
HungarianAlgorithm.Result res = HungarianAlgorithm.solve(cost);
42+
assertEquals(0, res.minCost);
43+
}
44+
}

0 commit comments

Comments
 (0)