This commit is contained in:
2026-07-04 19:27:17 +09:00
parent 34cd5ad2e7
commit ba6a0b2c17
30 changed files with 2935 additions and 247 deletions
+92
View File
@@ -0,0 +1,92 @@
defmodule Rsh26pool.Voting.HungarianTest do
use ExUnit.Case, async: true
alias Rsh26pool.Voting.Hungarian
describe "min_cost_assignment/1" do
test "empty matrix yields no cost and no assignment" do
assert Hungarian.min_cost_assignment([]) == {0, []}
end
test "raises when there are more rows than columns" do
assert_raise ArgumentError, fn ->
Hungarian.min_cost_assignment([[1, 2], [3, 4], [5, 6]])
end
end
test "square matrix matches the brute-force optimum" do
cost = [[4, 1, 3], [2, 0, 5], [3, 2, 2]]
{total, assignment} = Hungarian.min_cost_assignment(cost)
{brute_total, _} = brute_force(cost)
assert total == brute_total
assert valid_assignment?(assignment, 3, 3)
assert picked_sum(cost, assignment) == total
end
test "rectangular matrix (rows < cols) matches the brute-force optimum" do
cost = [[3, 1, 2, 4], [4, 2, 1, 0]]
{total, assignment} = Hungarian.min_cost_assignment(cost)
{brute_total, _} = brute_force(cost)
assert total == brute_total
assert valid_assignment?(assignment, 2, 4)
assert picked_sum(cost, assignment) == total
end
test "matches the brute-force optimum across many random small matrices" do
# Deterministic: fixed seed, and the failing matrix is printed for repro.
:rand.seed(:exsss, {13, 57, 911})
for _ <- 1..250 do
n = Enum.random(1..4)
m = Enum.random(n..5)
cost = for _ <- 1..n, do: for(_ <- 1..m, do: Enum.random(0..9))
{total, assignment} = Hungarian.min_cost_assignment(cost)
{brute_total, _} = brute_force(cost)
assert total == brute_total,
"cost=#{inspect(cost)} gave total #{total}, brute force found #{brute_total}"
assert valid_assignment?(assignment, n, m),
"cost=#{inspect(cost)} gave invalid assignment #{inspect(assignment)}"
assert picked_sum(cost, assignment) == total,
"cost=#{inspect(cost)}: reported total #{total} != summed picks for #{inspect(assignment)}"
end
end
end
# An assignment must be an injective mapping row -> column: length n, every
# value a distinct column in range.
defp valid_assignment?(assignment, n, m) do
length(assignment) == n and
Enum.all?(assignment, &(&1 >= 0 and &1 < m)) and
length(Enum.uniq(assignment)) == n
end
defp picked_sum(cost, assignment) do
assignment
|> Enum.with_index()
|> Enum.reduce(0, fn {col, row}, acc -> acc + (cost |> Enum.at(row) |> Enum.at(col)) end)
end
# Exhaustive optimum over every injective row->column mapping.
defp brute_force(cost) do
n = length(cost)
m = length(hd(cost))
cols = Enum.to_list(0..(m - 1))
cols
|> injective_maps(n)
|> Enum.map(fn assignment -> {picked_sum(cost, assignment), assignment} end)
|> Enum.min_by(&elem(&1, 0))
end
defp injective_maps(_cols, 0), do: [[]]
defp injective_maps(cols, k) do
for col <- cols, rest <- injective_maps(cols -- [col], k - 1), do: [col | rest]
end
end