Skip to content

Commit

Permalink
Graphs: Shortest Distance - A* and bidirectional A* (#227)
Browse files Browse the repository at this point in the history
  • Loading branch information
antonioaversa authored Oct 1, 2022
1 parent 5d3f9f1 commit 83a5bf8
Show file tree
Hide file tree
Showing 25 changed files with 470 additions and 34 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using MoreStructures.Graphs;

namespace MoreStructures.Tests.Graphs;

[TestClass]
public class DictionaryAdapterGraphDistancesTests
{
[TestMethod]
public void Indexer_TakesDataFromUnderlyingDictionary()
{
var dictionary = new Dictionary<(int, int), int>
{
[(0, 0)] = 0,
[(0, 1)] = 1,
[(0, 2)] = 2,
[(1, 0)] = 3,
};
var graphDistances = new DictionaryAdapterGraphDistances(dictionary);

foreach (var key in dictionary.Keys)
Assert.AreEqual(dictionary[key], graphDistances[key]);
}

[TestMethod]
public void Indexer_RaisesExceptionWhenProvidedEdgeIsUnknown()
{
var dictionary = new Dictionary<(int, int), int>
{
[(0, 0)] = 0,
[(0, 1)] = 1,
[(0, 2)] = 2,
[(1, 0)] = 3,
};
var graphDistances = new DictionaryAdapterGraphDistances(dictionary);

Assert.ThrowsException<KeyNotFoundException>(() => graphDistances[(1, 2)]);
Assert.ThrowsException<KeyNotFoundException>(() => graphDistances[(2, 0)]);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,8 @@ public void Find_IsCorrect(
var finder = FinderBuilder();

var distancesDict = starts.Zip(ends).Zip(distances).ToDictionary(t => t.First, t => t.Second);
var mst = finder.Find(graph, distancesDict);
var graphDistances = new DictionaryAdapterGraphDistances(distancesDict);
var mst = finder.Find(graph, graphDistances);

// If numberOfVertices == 0 => 0 edges in MST, 0 distinct vertices in MST, 0 connected components
if (numberOfVertices > 1)
Expand Down Expand Up @@ -119,7 +120,8 @@ public void Find_ThrowsExceptionIfTheGraphIsNotConnected(
var finder = FinderBuilder();

var distancesDict = starts.Zip(ends).Zip(distances).ToDictionary(t => t.First, t => t.Second);
var graphDistances = new DictionaryAdapterGraphDistances(distancesDict);

Assert.ThrowsException<InvalidOperationException>(() => finder.Find(graph, distancesDict), graphDescription);
Assert.ThrowsException<InvalidOperationException>(() => finder.Find(graph, graphDistances), graphDescription);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using MoreStructures.Graphs;
using MoreStructures.Graphs.ShortestDistance;
using MoreStructures.PriorityQueues.BinomialHeap;

namespace MoreStructures.Tests.Graphs.ShortestDistance;

[TestClass]
public class AStarShortestDistanceFinderTests_WithoutHeuristic : DijkstraShortestDistanceFinderTests
{
public AStarShortestDistanceFinderTests_WithoutHeuristic()
: base(
(numberOfVertices, edges) => new EdgeListGraph(numberOfVertices, edges),
() => new AStarShortestDistanceFinder(() => new UpdatableBinomialHeapPriorityQueue<int>()))
{
}
}

[TestClass]
public class AStarShortestDistanceFinderTests_WithHeuristic : PotentialBasedShortestDistanceFinderTests
{
public AStarShortestDistanceFinderTests_WithHeuristic()
: base(
(numberOfVertices, edges) => new EdgeListGraph(numberOfVertices, edges),
() => new AStarShortestDistanceFinder(() => new UpdatableBinomialHeapPriorityQueue<int>()))
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using MoreStructures.Graphs;
using MoreStructures.Graphs.ShortestDistance;
using MoreStructures.PriorityQueues.BinomialHeap;

namespace MoreStructures.Tests.Graphs.ShortestDistance;

[TestClass]
public class BidirectionalAStarShortestDistanceFinderTests_WithoutHeuristic : DijkstraShortestDistanceFinderTests
{
public BidirectionalAStarShortestDistanceFinderTests_WithoutHeuristic()
: base(
(numberOfVertices, edges) => new EdgeListGraph(numberOfVertices, edges),
() => new BidirectionalAStarShortestDistanceFinder(() => new UpdatableBinomialHeapPriorityQueue<int>()))
{
}
}

[TestClass]
public class BidirectionalAStarShortestDistanceFinderTests_WithHeuristic : PotentialBasedShortestDistanceFinderTests
{
public BidirectionalAStarShortestDistanceFinderTests_WithHeuristic()
: base(
(numberOfVertices, edges) => new EdgeListGraph(numberOfVertices, edges),
() => new BidirectionalAStarShortestDistanceFinder(() => new UpdatableBinomialHeapPriorityQueue<int>()))
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
using MoreStructures.Graphs;
using MoreStructures.Graphs.ShortestDistance;

namespace MoreStructures.Tests.Graphs.ShortestDistance;

public abstract class PotentialBasedShortestDistanceFinderTests
{
protected Func<int, IList<(int, int)>, IGraph> GraphBuilder { get; }
protected Func<IPotentialBasedShortestDistanceFinder> FinderBuilder { get; }

protected PotentialBasedShortestDistanceFinderTests(
Func<int, IList<(int, int)>, IGraph> graphBuilder, Func<IPotentialBasedShortestDistanceFinder> finderBuilder)
{
GraphBuilder = graphBuilder;
FinderBuilder = finderBuilder;
}

[DataRow("7 V, source to sink, same source to 1-chain and 3-chain merging to vertex to sink", 7,
new[] { 0, 0, 0, 1, 2, 3, 4, 5 },
new[] { 1, 2, 6, 3, 4, 6, 5, 1 },
new[] { 27, 3, 21, 3, 3, 3, 3, 3 },
new int[]
{
9, 8, 7, 6, 5, 4, 3, // Bad potentials: non-sensible values
1, 1, 1, 1, 1, 1, 1, // Bad potentials: all equal values
0, 3, 1, 4, 2, 3, 3, // Approx euclidean potentials calculated from vertex 0
3, 0, 3, 1, 2, 1, 1, // Approx euclidean potentials calculated from vertex 1
1, 3, 2, 1, 3, 2, 4, // Approx euclidean potentials calculated from vertex 2
})]
[DataTestMethod]
public void Find_IsCorrect(
string graphDescription, int numberOfVertices, int[] starts, int[] ends, int[] distances, int[] potentials)
{
var graph = GraphBuilder(numberOfVertices, starts.Zip(ends).ToList());
var graphDistances = new DictionaryAdapterGraphDistances(
starts.Zip(ends).Zip(distances).ToDictionary(t => t.First, t => t.Second));

var finder = FinderBuilder();

for (var start = 0; start < numberOfVertices; start++)
{
for (var end = 0; end < numberOfVertices; end++)
{
for (var i = 0; i < potentials.Length; i += numberOfVertices)
{
var (distanceWithHeuristic, pathWithHeuristic) =
finder.Find(graph, graphDistances, v => potentials[i + v], start, end);
var (distanceWithoutHeuristic, pathWithoutHeuristic) =
finder.Find(graph, graphDistances, start, end);
Assert.AreEqual(distanceWithoutHeuristic, distanceWithHeuristic, graphDescription);
Assert.IsTrue(pathWithoutHeuristic.SequenceEqual(pathWithHeuristic), graphDescription);
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -149,10 +149,11 @@ protected void TestGraph(
int expectedDistance, int[] expectedPath)
{
var graph = GraphBuilder(numberOfVertices, starts.Zip(ends).ToList());
var distancesDict = starts.Zip(ends).Zip(distances).ToDictionary(t => t.First, t => t.Second);
var graphDistances = new DictionaryAdapterGraphDistances(
starts.Zip(ends).Zip(distances).ToDictionary(t => t.First, t => t.Second));

var finder = FinderBuilder();
var (distance, path) = finder.Find(graph, distancesDict, start, end);
var (distance, path) = finder.Find(graph, graphDistances, start, end);
var message =
$"{graphDescription} - Expected [{string.Join(", ", expectedPath)}], Actual: [{string.Join(", ", path)}]";
Assert.AreEqual(expectedDistance, distance, message);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,11 @@ protected void TestGraph(
string graphDescription, int numberOfVertices, int[] starts, int[] ends, int[] distances, int start)
{
var graph = GraphBuilder(numberOfVertices, starts.Zip(ends).ToList());
var distancesDict = starts.Zip(ends).Zip(distances).ToDictionary(t => t.First, t => t.Second);
var graphDistances = new DictionaryAdapterGraphDistances(
starts.Zip(ends).Zip(distances).ToDictionary(t => t.First, t => t.Second));

var finder = FinderBuilder();
var bestPreviouses = finder.FindTree(graph, distancesDict, start).Values;
var bestPreviouses = finder.FindTree(graph, graphDistances, start).Values;
var bestDistances =
from bp in bestPreviouses
orderby bp.Key
Expand All @@ -48,7 +49,7 @@ orderby bp.Key
var singlePathFinder = SinglePathFinderBuilder();
var expectedBestDistances = Enumerable
.Range(0, numberOfVertices)
.Select(v => singlePathFinder.Find(graph, distancesDict, start, v).Item1)
.Select(v => singlePathFinder.Find(graph, graphDistances, start, v).Item1)
.Where(d => d < int.MaxValue);

var message =
Expand Down Expand Up @@ -80,7 +81,7 @@ orderby bp.Key

Assert.IsTrue(path.First.Value == start);

var expectedPathDistance = path.Zip(path.Skip(1)).Sum(e => distancesDict[e]);
var expectedPathDistance = path.Zip(path.Skip(1)).Sum(e => graphDistances[e]);
Assert.AreEqual(bestPreviouses[vertex].DistanceFromStart, expectedPathDistance);
}
}
Expand Down
26 changes: 26 additions & 0 deletions MoreStructures/Graphs/DictionaryAdapterGraphDistances.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
namespace MoreStructures.Graphs;

/// <summary>
/// A <see cref="IGraphDistances"/> retrieving distances from a <see cref="IDictionary{TKey, TValue}"/>, mapping
/// couples of <see cref="int"/> values (ids of endpoints of each edge of the graph) to <see cref="int"/> values
/// (edge distances).
/// </summary>
public class DictionaryAdapterGraphDistances : IGraphDistances
{
private IDictionary<(int, int), int> Dictionary { get; }

/// <inheritdoc path="//*[not(self::remarks)]"/>
/// <remarks>
/// Retrieves the value from the underlying dictionary.
/// </remarks>
public int this[(int edgeStart, int edgeEnd) edge] => Dictionary[edge];

/// <summary>
/// <inheritdoc cref="DictionaryAdapterGraphDistances"/>
/// </summary>
/// <param name="dictionary">The mapping between edges and distances.</param>
public DictionaryAdapterGraphDistances(IDictionary<(int, int), int> dictionary)
{
Dictionary = dictionary;
}
}
15 changes: 15 additions & 0 deletions MoreStructures/Graphs/IGraphDistances.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace MoreStructures.Graphs;

/// <summary>
/// Represents a mapping between edges of a <see cref="IGraph"/> and distances, in a spatial context, or weights, in
/// a more general setting.
/// </summary>
public interface IGraphDistances
{
/// <summary>
/// Returns the distance, or weight, of the provided <paramref name="edge"/>.
/// </summary>
/// <param name="edge">The edge, to provide the distance of.</param>
/// <returns>Any positive or negative number.</returns>
int this[(int edgeStart, int edgeEnd) edge] { get; }
}
4 changes: 2 additions & 2 deletions MoreStructures/Graphs/MinimumSpanningTree/IMstFinder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,12 @@ public interface IMstFinder
/// </summary>
/// <param name="graph">The <see cref="IGraph"/>, to find the MST of.</param>
/// <param name="distances">
/// The dictionary mapping each edge of <paramref name="graph"/> to its weight, which represents the "distance"
/// The mapping of each edge of <paramref name="graph"/> to its weight, which represents the "distance"
/// from the start vertex of the edge to the end vertex.
/// </param>
/// <returns>The set of edges, in the form (source, target), identifying the MST.</returns>
/// <remarks>
/// <inheritdoc cref="IMstFinder"/>
/// </remarks>
public ISet<(int, int)> Find(IGraph graph, IDictionary<(int, int), int> distances);
public ISet<(int, int)> Find(IGraph graph, IGraphDistances distances);
}
6 changes: 3 additions & 3 deletions MoreStructures/Graphs/MinimumSpanningTree/KruskalMstFinder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ public KruskalMstFinder(IInPlaceSorting sorter, Func<int, IDisjointSet> disjoint
/// <remarks>
/// <inheritdoc cref="KruskalMstFinder"/>
/// </remarks>
public ISet<(int, int)> Find(IGraph graph, IDictionary<(int, int), int> distances)
public ISet<(int, int)> Find(IGraph graph, IGraphDistances distances)
{
var edges = graph.GetAllEdges().ToList();
Sorter.Sort(edges, new EdgesComparer(distances));
Expand All @@ -132,9 +132,9 @@ public KruskalMstFinder(IInPlaceSorting sorter, Func<int, IDisjointSet> disjoint

private sealed class EdgesComparer : IComparer<(int, int)>
{
private IDictionary<(int, int), int> Distances { get; }
private IGraphDistances Distances { get; }

public EdgesComparer(IDictionary<(int, int), int> distances)
public EdgesComparer(IGraphDistances distances)
{
Distances = distances;
}
Expand Down
2 changes: 1 addition & 1 deletion MoreStructures/Graphs/MinimumSpanningTree/PrimMstFinder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ public PrimMstFinder(Func<IUpdatablePriorityQueue<int>> priorityQueueBuilder)
/// <remarks>
/// <inheritdoc cref="PrimMstFinder"/>
/// </remarks>
public ISet<(int, int)> Find(IGraph graph, IDictionary<(int, int), int> distances)
public ISet<(int, int)> Find(IGraph graph, IGraphDistances distances)
{
var numberOfVertices = graph.GetNumberOfVertices();
if (numberOfVertices == 0)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
using MoreStructures.PriorityQueues;

namespace MoreStructures.Graphs.ShortestDistance;

/// <summary>
/// A <see cref="IShortestDistanceFinder"/> implementation based on the A* algorithm, which is a refinement of
/// the Dijkstra's algorithm, introducing a goal-oriented heuristic, driving the search in the direction of the end
/// vertex.
/// </summary>
/// <remarks>
/// <para id="requirements">
/// <inheritdoc cref="DijkstraShortestDistanceFinder" path="/remarks/para[@id='requirements']"/>
/// </para>
/// <para id="algorithm">
/// ALGORITHM
/// <br/>
/// - The algorithm is described in <see cref="DijkstraShortestDistanceFinder"/>, with the only difference that
/// edge distances are modified, based on a heuristic defined as a potential function.
/// <br/>
/// - New edge distance is defined as follow: given the potential function P, for each edge (u, v) in the graph,
/// <c>d'(u, v) = d(u, v) + P(v) - P(u)</c>.
/// <br/>
/// - If P is defined correctly, P(u) and P(v) are good approximations of the distance of u and v from the end
/// vertex e.
/// <br/>
/// - If so, <c>P(v) - P(u)</c> will be negative if moving from u to v gets us closer to e and positive if it
/// gets us farther from it.
/// <br/>
/// - For this reason, given two vertices v' and v'' connected from u via <c>e' = (u, v')</c> and
/// <c>e'' = (u, v'')</c>, and such that <c>d(e') = d(e'')</c>, if <c>P(v') &lt; P(v'')</c> then
/// <c>d'(e') &lt; d''(e')</c>, so the algorithm will prefer e' over e'' during the exploration, as it seems to
/// be closer to e.
/// <br/>
/// - Because the algorithm stops when e is processed, if the algorithm visits e earlier than later, the algorithm
/// will find the shortest path from s to e ealier than later.
/// </para>
/// <para id="complexity">
/// COMPLEXITY
/// <br/>
/// - The complexity heavily depends on the accuracy of the potential function.
/// <br/>
/// - A good model of the average performance of the algorithm is very complicated to derive, since the heuristic
/// can drive the exploration much quicker or slower towards the end vertex, depending on how the function is
/// defined.
/// <br/>
/// - In general, potential functions which are closer to the actual shortest distance to the end vertex yield
/// better results. The farther they move from the ideal, the less optimized the exploration of the graph
/// becomes.
/// <br/>
/// - Worst case remains as in <see cref="DijkstraShortestDistanceFinder"/>, where all vertices of the graph have
/// to be explored, for a path from the start to the end to be found (or prove there is no path, since start and
/// end lie in two different connected components).
/// <br/>
/// - Best case is when P is the shortest distance to e, in which case only the vertices of a shortest path from
/// s to e are visited (which is tipically a small fraction of the vertices of the graph, especially if the graph
/// is large). That is the bare minimum to find the shortest path from s to e.
/// <br/>
/// - Average case can even be worse than normal Dijkstra, if P is misleading, i.e. if it drives the exploration
/// away from e, rather than reducing the cost of edges which drives the exploration closer to e.
/// <br/>
/// - However, with a well defined P, close enough to the actual shortest distance, Time Complexity is between
/// O(e + v * log(v)) and O(h), where h is the highest number of edges of a shortest path from s to e.
/// <br/>
/// - Space Complexity is also between O(h) and O(v).
/// </para>
/// </remarks>
public class AStarShortestDistanceFinder : PotentialBasedShortestDistanceFinder
{
/// <inheritdoc cref="AStarShortestDistanceFinder"/>
/// <param name="priorityQueueBuilder">
/// A builder of a <see cref="IUpdatablePriorityQueue{T}"/> of <see cref="int"/> values, used by the algorithm to
/// store edges with priority from the closest to the start, to the farthest.
/// </param>
public AStarShortestDistanceFinder(Func<IUpdatablePriorityQueue<int>> priorityQueueBuilder)
: base(new DijkstraShortestDistanceFinder(priorityQueueBuilder))
{
}
}
Loading

0 comments on commit 83a5bf8

Please sign in to comment.