Skip to content

Commit

Permalink
Merge pull request #20 from Gnimuc/infinite-loop
Browse files Browse the repository at this point in the history
Fix #18 by explicitly converting `Inf` to `prevfloat(Inf)`
  • Loading branch information
Gnimuc authored Jan 2, 2023
2 parents ff82ca3 + f154198 commit 97ad440
Show file tree
Hide file tree
Showing 3 changed files with 70 additions and 21 deletions.
73 changes: 52 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -39,47 +39,78 @@ 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

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
Expand Down
10 changes: 10 additions & 0 deletions src/Munkres.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
8 changes: 8 additions & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down

0 comments on commit 97ad440

Please sign in to comment.