Skip to content

Commit

Permalink
Update references and attribution
Browse files Browse the repository at this point in the history
  • Loading branch information
fuglede committed Jun 30, 2019
1 parent ea68ec5 commit 737fea0
Show file tree
Hide file tree
Showing 4 changed files with 34 additions and 28 deletions.
23 changes: 14 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,33 +1,38 @@
# Linear assignment problem in .NET
[![Build status](https://travis-ci.org/fuglede/linearassignment.svg?branch=master)](https://travis-ci.org/fuglede/linearassignment)
# Linear assignment problem solver in .NET

This repository includes a pure C# solver for the rectangular [linear assignment problem](https://en.wikipedia.org/wiki/Assignment_problem), also known as the minimum weight full matching problem for bipartite graphs.
This repository includes a pure C# solver for the rectangular [linear assignment problem](https://en.wikipedia.org/wiki/Assignment_problem), also known as the [minimum weight full matching](https://en.wikipedia.org/wiki/Maximum_weight_matching) for [bipartite graphs](https://en.wikipedia.org/wiki/Bipartite_graph).


## The problem

Concretely, the problem we solve is the following: Let *G* = (*V*, *E*) be a graph and assume that *V* is the disjoint union of two sets *X* and *Y*, with |*X*| ≤ |*Y*|, and so that for every (*i*, *j*) ∈ *E* we have *i**X*, *j**Y*. A full matching *M**E* is a subset of edges with the property that every vertex in *X* belongs to exactly one edge in *M*, and every vertex in *Y* belongs to at most one edge in *M*. Let *c* : *E***R** be any real function. Our goal is to find a full matching *M* minimizing Σ<sub>*e**M*</sub> *c*(*e*).
Concretely, the problem we solve is the following: Let *G* = (*V*, *E*) be a [graph](https://en.wikipedia.org/wiki/Graph_(discrete_mathematics)) and assume that *V* is the disjoint union of two sets *X* and *Y*, with |*X*| ≤ |*Y*|, and that for every (*i*, *j*) ∈ *E* we have *i**X*, *j**Y*. A full matching *M**E* is a subset of edges with the property that every vertex in *X* belongs to at most one edge in *M*, and every vertex in *Y* belongs to exactly one edge in *M*. Let *c* : *E***R** be any real function. Our goal is to find a full matching *M* minimizing Σ<sub>*e**M*</sub> *c*(*e*).


## Method

The method we use is a modified version of the Jonker--Volgenant algorithm as described in
The method we use is based on shortest augmenting paths: At each step of the algorithm, the matching *M* is expanded by using Dijkstra's algorithm to find the shortest augmenting path from a given non-matched element of *X* to a non-matched element of *Y*, and the weights of the graph are updated according to a primal-dual method. We follow the pseudo-code laid out in

DF Crouse. On implementing 2D rectangular assignment algorithms.
IEEE Transactions on Aerospace and Electronic Systems
52(4):1679-1696, August 2016
doi: 10.1109/TAES.2016.140952

and ported from the C++ implementation in [`scipy.optimize`](https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.linear_sum_assignment.html).
which in turn in based closely on Section 14.4 of

The algorithm is a greatly simplified version of the original algorithm as described in
Rainer Burkard, Mauro Dell'Amico, Silvano Martello.
Assignment Problems - Revised Reprint
Society for Industrial and Applied Mathematics, Philadelphia, 2012

In Crouse's approach, the main reference is

R. Jonker and A. Volgenant. A Shortest Augmenting Path Algorithm for
Dense and Sparse Linear Assignment Problems. *Computing*, 38:325-340
December 1987.

Here, most of the computational time is spent on initialization and setting up a useful solution prior to searching for shortest augmenting paths, where in the method at hand, all initialization is skipped, and we jump straight to augmentation.

Our algorithm is ported from the C++ implementation in [`scipy.optimize`](https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.linear_sum_assignment.html).


## Installation

Expand All @@ -40,7 +45,7 @@ With the notation above, assume that *X* consists of 3 vertices, and that *Y* co

```cs
var cost = new[,] {{400, 150, 400, 1}, {400, 450, 600, 2}, {300, 225, 300, double.PositiveInfinity}};
var res = JonkerVolgenant.Solve(cost).ColumnAssignment;
var res = Solver.Solve(cost).ColumnAssignment;
```

The result is `{1, 3, 2}` indicating that the three vertices of *X* are matched with the second, fourth, and third vertex in *Y* respectively (noting the zero-indexing).
The result is `{1, 3, 2}` indicating that the three vertices of *X* are matched with the second, fourth, and third vertex in *Y* respectively (noting the zero-indexing).
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

namespace LinearAssignment.Tests
{
public class JonkerVolgenantTest
public class SolverTest
{
[Theory]
[MemberData(nameof(TestData))]
Expand All @@ -15,7 +15,7 @@ public void SolveGivesExpectedResult(
double[] expectedDualU,
double[] expectedDualV)
{
var solution = JonkerVolgenant.Solve(cost);
var solution = Solver.Solve(cost);
Assert.Equal(expectedColumnAssignment, solution.ColumnAssignment);
Assert.Equal(expectedRowAssignment, solution.RowAssignment);
Assert.Equal(expectedDualU, solution.DualU);
Expand Down Expand Up @@ -86,21 +86,21 @@ public void SolveGivesExpectedResult(
public void SolveThrowsOnInputWithMoreRowsThanColumns()
{
var cost = new[,] {{1d},{2d}};
Assert.Throws<ArgumentException>(() => JonkerVolgenant.Solve(cost));
Assert.Throws<ArgumentException>(() => Solver.Solve(cost));
}

[Fact]
public void SolveThrowsOnNegativeInput()
{
var cost = new[,] {{-1d}};
Assert.Throws<ArgumentException>(() => JonkerVolgenant.Solve(cost));
Assert.Throws<ArgumentException>(() => Solver.Solve(cost));
}

[Fact]
public void SolveThrowsWhenNoFeasibleSolutionExists()
{
var cost = new[,] {{double.PositiveInfinity}};
Assert.Throws<InvalidOperationException>(() => JonkerVolgenant.Solve(cost));
Assert.Throws<InvalidOperationException>(() => Solver.Solve(cost));
}
}
}
4 changes: 2 additions & 2 deletions src/LinearAssignment/LinearAssignment.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
<TargetFramework>netstandard2.0</TargetFramework>
<PackageId>LinearAssignment</PackageId>
<Title>Linear assignment problem solver</Title>
<Description>C#-only solver for the rectangular linear assignment problem, also known as the minimum weight full matching for bipartite graphs, based on the Jonker--Volgenant algorithm.</Description>
<Version>1.0.2</Version>
<Description>C#-only solver for the rectangular linear assignment problem, also known as the minimum weight full matching problem for bipartite graphs. The algorithm is based on shortest augmenting path search.</Description>
<Version>1.0.3</Version>
<Authors>fuglede</Authors>
<PackageLicenseExpression>BSD-3-Clause</PackageLicenseExpression>
<PackageTags>math;optimization;graph-theory;computer-science;algorithm;combinatorics;matching;operations-research;solver</PackageTags>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,24 @@
namespace LinearAssignment
{
/// <summary>
/// Solver for the linear assignment problem based on the Jonker--Volgenant algorithm:
///
/// R. Jonker and A. Volgenant. A Shortest Augmenting Path Algorithm for
/// Dense and Sparse Linear Assignment Problems. *Computing*, 38:325-340
/// December 1987.
///
/// This particular implementation is based on a simplified version of the algorithm
/// described in
/// Solver for the linear assignment problem based on shortest augmenting paths. Concretely,
/// we implement the pseudo-code from
///
/// DF Crouse. On implementing 2D rectangular assignment algorithms.
/// IEEE Transactions on Aerospace and Electronic Systems
/// 52(4):1679-1696, August 2016
/// doi: 10.1109/TAES.2016.140952
///
/// Concretely, this is a C# port of the C++ implementation of the algorithm by Peter
/// Mahler Larsen included in the Python library scipy.optimize.
/// which in turn is based closely on Section 14.4 of
///
/// Rainer Burkard, Mauro Dell'Amico, Silvano Martello.
/// Assignment Problems - Revised Reprint
/// Society for Industrial and Applied Mathematics, Philadelphia, 2012
///
/// This is a C# port of the C++ implementation of the algorithm by Peter Mahler Larsen included
/// in the Python library scipy.optimize. https://github.com/scipy/scipy/pull/10296/
/// </summary>
public static class JonkerVolgenant
public static class Solver
{
public static Assignment Solve(double[,] cost, bool skipPositivityTest = false)
{
Expand Down Expand Up @@ -94,13 +94,14 @@ public static Assignment Solve(double[,] cost, bool skipPositivityTest = false)

minVal = lowest;
var jp = remaining[indexLowest];
sc[jp] = true;
if (double.IsPositiveInfinity(minVal))
throw new InvalidOperationException("No feasible solution.");
if (y[jp] == -1)
sink = jp;
else
i = y[jp];

sc[jp] = true;
remaining[indexLowest] = remaining[--numRemaining];
remaining.RemoveAt(numRemaining);
}
Expand Down

0 comments on commit 737fea0

Please sign in to comment.