From 7d9f3227b7524c25f41d4d2fd687b6df40ae756f Mon Sep 17 00:00:00 2001 From: Gnimuc Date: Mon, 2 Jan 2023 17:36:41 +0900 Subject: [PATCH 1/4] Convert `Inf` to `prevfloat(Inf)` explicitly to avoid infinite loop --- src/Munkres.jl | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Munkres.jl b/src/Munkres.jl index 9abbbfe..72ceb15 100644 --- a/src/Munkres.jl +++ b/src/Munkres.jl @@ -31,6 +31,9 @@ function munkres(costMat::AbstractMatrix{T}) where {T<:Real} colNum ≥ rowNum || throw(ArgumentError("Non-square matrix should have more columns than rows.")) + # Inf values can cause infinite loop, so we replace them with the largest finite value `prevfloat(typemax(T))`. + inf2finite!(costMat) + # preliminaries: # "no lines are covered;" rowCovered = falses(rowNum) @@ -139,6 +142,13 @@ function munkres(costMat::AbstractMatrix{T}) where {T<:Real} return Zs end +function inf2finite!(costMat::AbstractMatrix{T}) where {T<:AbstractFloat} + for i in eachindex(costMat) + @inbounds costMat[i] = ifelse(isinf(costMat[i]), prevfloat(typemax(T)), costMat[i]) + end +end +inf2finite!(costMat::AbstractMatrix{T}) where {T<:Real} = nothing + function munkres(costMat::AbstractMatrix{S}) where {T<:Real,S<:Union{Missing,T}} # replace forbidden edges (i.e. those with a missing cost) by a very large cost, so that they # are never chosen for the matching (except if they are the only possible edges) From 7d9d77fd8ea87d7a14d3c2afdae3991bf4a85937 Mon Sep 17 00:00:00 2001 From: Thibaut Cuvelier Date: Thu, 29 Dec 2022 14:24:07 +0100 Subject: [PATCH 2/4] Add an example for `missing` with `hungarian` --- README.md | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 475e038..ee8be8b 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,22 @@ julia> assignment, cost = hungarian(weights) ``` Since each worker can perform only one task and each task can be assigned to only one worker, those `0`s in the `assignment` mean that no task is assigned to those workers. -# Usage +If a job-worker assignment is not possible, use the special `missing` value to indicate which pairs are disallowed: + +```julia +julia> using Hungarian + +julia> weights = [missing 1 1; 1 0 1; 1 1 0] +3×3 Matrix{Union{Missing, Int64}}: + missing 1 1 + 1 0 1 + 1 1 0 + +julia> assignment, cost = hungarian(weights) +([2, 1, 3], 2) +``` + +## Usage When solving a canonical assignment problem, namely, the cost matrix is square, one can directly get the matching via `Hungarian.munkres(x)` instead of `hungarian(x)`: ```julia julia> using Hungarian From 14a9ea52212c205033777f600eedd516e6a431a1 Mon Sep 17 00:00:00 2001 From: Gnimuc Date: Mon, 2 Jan 2023 17:47:31 +0900 Subject: [PATCH 3/4] Update README.md --- README.md | 56 +++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 36 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index ee8be8b..9fe20e2 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ We can solve the assignment problem by: julia> using Hungarian julia> assignment, cost = hungarian(weights) -([2,1,0,0,3],8) +([2, 1, 0, 0, 3], 8) # worker 1 => task 2 with weights[1,2] = 1 # worker 2 => task 1 with weights[2,1] = 5 @@ -60,41 +60,57 @@ When solving a canonical assignment problem, namely, the cost matrix is square, julia> using Hungarian julia> matching = Hungarian.munkres(rand(5,5)) -5×5 SparseArrays.SparseMatrixCSC{Int8,Int64} with 7 stored entries: - [1, 1] = 1 - [5, 1] = 2 - [1, 2] = 2 - [2, 3] = 2 - [2, 4] = 1 - [3, 4] = 2 - [4, 5] = 2 +5×5 SparseArrays.SparseMatrixCSC{Int8, Int64} with 9 stored entries: + 1 2 ⋅ ⋅ ⋅ + ⋅ ⋅ 1 ⋅ 2 + 2 ⋅ ⋅ ⋅ ⋅ + 1 ⋅ 2 ⋅ ⋅ + ⋅ ⋅ ⋅ 2 1 # 0 => non-zero # 1 => zero # 2 => STAR julia> Matrix(matching) -5×5 Array{Int8,2}: +5×5 Matrix{Int8}: 1 2 0 0 0 - 0 0 2 1 0 - 0 0 0 2 0 - 0 0 0 0 2 + 0 0 1 0 2 2 0 0 0 0 + 1 0 2 0 0 + 0 0 0 2 1 julia> [findfirst(matching[i,:].==Hungarian.STAR) for i = 1:5] -5-element Array{Int64,1}: +5-element Vector{Int64}: 2 - 3 - 4 5 1 + 3 + 4 julia> [findfirst(matching[:,i].==Hungarian.STAR) for i = 1:5] -5-element Array{Int64,1}: - 5 - 1 - 2 +5-element Vector{Int64}: 3 + 1 4 + 5 + 2 +``` + +If a job-worker assignment is not possible, use the special `missing` value to indicate which pairs are disallowed: + +```julia +julia> using Hungarian + +julia> weights = [missing 1 1; 1 0 1; 1 1 0] +3×3 Matrix{Union{Missing, Int64}}: + missing 1 1 + 1 0 1 + 1 1 0 + +julia> matching = Hungarian.munkres(weights) +3×3 SparseArrays.SparseMatrixCSC{Int8, Int64} with 6 stored entries: + ⋅ 2 1 + 2 1 ⋅ + 1 ⋅ 2 ``` ## References From f15419869f71e6b702994348bfcc3ffc4c6f8339 Mon Sep 17 00:00:00 2001 From: Thibaut Cuvelier Date: Thu, 29 Dec 2022 02:39:43 +0100 Subject: [PATCH 4/4] Add a test case from the issue. --- test/runtests.jl | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/runtests.jl b/test/runtests.jl index 0eb97e0..8d30541 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -67,6 +67,14 @@ end @test cost == 71139 end +@testset "issue #18" begin + # These instances used to run into infinite loops. + weights = [1.0 Inf; Inf Inf] + result = Hungarian.munkres(weights) + assign, cost = hungarian(weights) + @test cost == prevfloat(Inf) +end + @testset "UInt8" begin M=UInt8[67 228 135 197 244; 112 44 84 206 31;