diff --git a/Graph/DirectedWeightedGraph.cs b/Graph/DirectedWeightedGraph.cs index 58a1827..bbc6ec2 100644 --- a/Graph/DirectedWeightedGraph.cs +++ b/Graph/DirectedWeightedGraph.cs @@ -113,8 +113,38 @@ namespace MoonTools.Core.Graph yield break; } + private IEnumerable<(TNode, TNode)> ShortestPath(TNode start, TNode end, Func> SSSPAlgorithm) + { + CheckNodes(start, end); + + var cameFrom = new PooledDictionary(ClearMode.Always); + var reachable = new PooledSet(ClearMode.Always); + + foreach (var (node, previous, weight) in SSSPAlgorithm(start)) + { + cameFrom[node] = previous; + reachable.Add(node); + } + + if (!reachable.Contains(end)) + { + cameFrom.Dispose(); + reachable.Dispose(); + yield break; + } + + foreach (var edge in ReconstructPath(cameFrom, end).Reverse()) + { + yield return edge; + } + + cameFrom.Dispose(); + reachable.Dispose(); + } + public IEnumerable<(TNode, TNode, int)> DijkstraSingleSourceShortestPath(TNode source) { + if (weights.Values.Any(w => w < 0)) { throw new NegativeWeightException("Dijkstra cannot be used on a graph with negative edge weights. Try Bellman-Ford"); } CheckNodes(source); var distance = new PooledDictionary(ClearMode.Always); @@ -148,7 +178,7 @@ namespace MoonTools.Core.Graph foreach (var node in Nodes) { - if (!node.Equals(source)) + if (previous.ContainsKey(node) && distance.ContainsKey(node)) { yield return (node, previous[node], distance[node]); } @@ -157,5 +187,62 @@ namespace MoonTools.Core.Graph distance.Dispose(); previous.Dispose(); } + + public IEnumerable<(TNode, TNode)> DijkstraShortestPath(TNode start, TNode end) + { + return ShortestPath(start, end, DijkstraSingleSourceShortestPath); + } + + public IEnumerable<(TNode, TNode, int)> BellmanFordSingleSourceShortestPath(TNode source) + { + CheckNodes(source); + + var distance = new PooledDictionary(ClearMode.Always); + var previous = new PooledDictionary(ClearMode.Always); + + foreach (var node in Nodes) + { + distance[node] = int.MaxValue; + } + + distance[source] = 0; + + for (int i = 0; i < Order; i++) + { + foreach (var (v, u) in Edges) + { + var weight = Weight(v, u); + if (distance[v] + weight < distance[u]) + { + distance[u] = distance[v] + weight; + previous[u] = v; + } + } + } + + foreach (var (v, u) in Edges) + { + if (distance[v] + Weight(v, u) < distance[u]) + { + throw new NegativeCycleException(); + } + } + + foreach (var node in Nodes) + { + if (previous.ContainsKey(node) && distance.ContainsKey(node)) + { + yield return (node, previous[node], distance[node]); + } + } + + distance.Dispose(); + previous.Dispose(); + } + + public IEnumerable<(TNode, TNode)> BellmanFordShortestPath(TNode start, TNode end) + { + return ShortestPath(start, end, BellmanFordSingleSourceShortestPath); + } } } \ No newline at end of file diff --git a/Graph/NegativeCycleException.cs b/Graph/NegativeCycleException.cs new file mode 100644 index 0000000..b71aa2e --- /dev/null +++ b/Graph/NegativeCycleException.cs @@ -0,0 +1,25 @@ +using System; +using System.Runtime.Serialization; + +namespace MoonTools.Core.Graph +{ + [Serializable] + public class NegativeCycleException : Exception + { + public NegativeCycleException() + { + } + + public NegativeCycleException(string message) : base(message) + { + } + + public NegativeCycleException(string message, Exception innerException) : base(message, innerException) + { + } + + protected NegativeCycleException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } + } +} \ No newline at end of file diff --git a/Graph/NegativeWeightException.cs b/Graph/NegativeWeightException.cs new file mode 100644 index 0000000..4aa2ca4 --- /dev/null +++ b/Graph/NegativeWeightException.cs @@ -0,0 +1,25 @@ +using System; +using System.Runtime.Serialization; + +namespace MoonTools.Core.Graph +{ + [Serializable] + public class NegativeWeightException : Exception + { + public NegativeWeightException() + { + } + + public NegativeWeightException(string message) : base(message) + { + } + + public NegativeWeightException(string message, Exception innerException) : base(message, innerException) + { + } + + protected NegativeWeightException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } + } +} \ No newline at end of file diff --git a/test/DirectedWeightedGraph.cs b/test/DirectedWeightedGraph.cs index f204567..825c5a3 100644 --- a/test/DirectedWeightedGraph.cs +++ b/test/DirectedWeightedGraph.cs @@ -264,5 +264,273 @@ namespace Tests // have to call Count() because otherwise the lazy evaluation wont trigger myGraph.Invoking(x => x.DijkstraSingleSourceShortestPath('z').Count()).Should().Throw(); } + + [Test] + public void DijkstraShortestPath() + { + var run = new MoveTypeEdgeData { moveType = MoveType.Run }; + var jump = new MoveTypeEdgeData { moveType = MoveType.Jump }; + var wallJump = new MoveTypeEdgeData { moveType = MoveType.WallJump }; + + var myGraph = new DirectedWeightedGraph(); + myGraph.AddNodes('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'); + myGraph.AddEdges( + ('a', 'b', 2, run), + ('a', 'c', 3, run), + ('a', 'e', 4, wallJump), + ('b', 'd', 2, jump), + ('b', 'e', 1, run), + ('c', 'g', 4, jump), + ('c', 'h', 11, run), + ('d', 'c', 3, jump), + ('d', 'f', 2, run), + ('d', 'h', 3, wallJump), + ('e', 'f', 5, run), + ('f', 'd', 2, run), + ('f', 'h', 6, wallJump), + ('g', 'h', 7, run), + ('h', 'f', 1, jump) + ); + + myGraph + .DijkstraShortestPath('a', 'h') + .Select(pair => myGraph.EdgeData(pair.Item1, pair.Item2)) + .Should() + .ContainInOrder( + run, jump, wallJump + ) + .And + .HaveCount(3); + + // have to call Count() because otherwise the lazy evaluation wont trigger + myGraph.Invoking(x => x.DijkstraShortestPath('a', 'z').Count()).Should().Throw(); + } + + [Test] + public void DijkstraNotReachable() + { + var run = new MoveTypeEdgeData { moveType = MoveType.Run }; + var jump = new MoveTypeEdgeData { moveType = MoveType.Jump }; + var wallJump = new MoveTypeEdgeData { moveType = MoveType.WallJump }; + + var myGraph = new DirectedWeightedGraph(); + myGraph.AddNodes('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'); + myGraph.AddEdges( + ('a', 'b', 2, run), + ('a', 'c', 3, run), + ('a', 'e', 4, wallJump), + ('b', 'd', 2, jump), + ('b', 'e', 1, run), + ('c', 'g', 4, jump), + ('c', 'h', 11, run), + ('d', 'c', 3, jump), + ('d', 'h', 3, wallJump), + ('f', 'd', 2, run), + ('f', 'h', 6, wallJump), + ('g', 'h', 7, run) + ); + + myGraph + .DijkstraShortestPath('a', 'f') + .Should() + .BeEmpty(); + } + + [Test] + public void DijkstraNegativeWeight() + { + var run = new MoveTypeEdgeData { moveType = MoveType.Run }; + var jump = new MoveTypeEdgeData { moveType = MoveType.Jump }; + var wallJump = new MoveTypeEdgeData { moveType = MoveType.WallJump }; + + var myGraph = new DirectedWeightedGraph(); + myGraph.AddNodes('a', 'b', 'c', 'd'); + myGraph.AddEdges( + ('a', 'b', -2, run), + ('a', 'c', 3, run), + ('b', 'd', 2, jump), + ('d', 'c', -3, jump) + ); + + myGraph + .Invoking(x => x.DijkstraShortestPath('a', 'd').Count()) + .Should() + .Throw(); + } + + [Test] + public void BellmanFordSingleSourceShortestPath() + { + var run = new MoveTypeEdgeData { moveType = MoveType.Run }; + var jump = new MoveTypeEdgeData { moveType = MoveType.Jump }; + var wallJump = new MoveTypeEdgeData { moveType = MoveType.WallJump }; + + var myGraph = new DirectedWeightedGraph(); + myGraph.AddNodes('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'); + myGraph.AddEdges( + ('a', 'b', 2, run), + ('a', 'c', 3, run), + ('a', 'e', 4, wallJump), + ('b', 'd', 2, jump), + ('b', 'e', 1, run), + ('c', 'g', 4, jump), + ('c', 'h', 11, run), + ('d', 'c', 3, jump), + ('d', 'f', 2, run), + ('d', 'h', 3, wallJump), + ('e', 'f', 5, run), + ('f', 'd', 2, run), + ('f', 'h', 6, wallJump), + ('g', 'h', 7, run), + ('h', 'f', 1, jump) + ); + + myGraph + .BellmanFordSingleSourceShortestPath('a') + .Should() + .Contain(('b', 'a', 2)).And + .Contain(('c', 'a', 3)).And + .Contain(('d', 'b', 4)).And + .Contain(('e', 'b', 3)).And + .Contain(('f', 'd', 6)).And + .Contain(('g', 'c', 7)).And + .Contain(('h', 'd', 7)).And + .HaveCount(7); + + // have to call Count() because otherwise the lazy evaluation wont trigger + myGraph.Invoking(x => x.BellmanFordSingleSourceShortestPath('z').Count()).Should().Throw(); + } + + [Test] + public void BellmanFordSingleSourceShortestPathWithNegative() + { + var run = new MoveTypeEdgeData { moveType = MoveType.Run }; + var jump = new MoveTypeEdgeData { moveType = MoveType.Jump }; + var wallJump = new MoveTypeEdgeData { moveType = MoveType.WallJump }; + + var myGraph = new DirectedWeightedGraph(); + myGraph.AddNodes('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'); + myGraph.AddEdges( + ('a', 'b', 2, run), + ('a', 'c', 3, run), + ('a', 'e', 4, wallJump), + ('b', 'd', -1, jump), + ('b', 'e', 1, run), + ('c', 'g', 4, jump), + ('c', 'h', 11, run), + ('d', 'c', 3, jump), + ('d', 'f', 2, run), + ('d', 'h', 3, wallJump), + ('e', 'f', 5, run), + ('f', 'd', 2, run), + ('f', 'h', 6, wallJump), + ('g', 'h', 7, run), + ('h', 'f', 1, jump) + ); + + myGraph + .BellmanFordSingleSourceShortestPath('a') + .Should() + .Contain(('b', 'a', 2)).And + .Contain(('c', 'a', 3)).And + .Contain(('d', 'b', 1)).And + .Contain(('e', 'b', 3)).And + .Contain(('f', 'd', 3)).And + .Contain(('g', 'c', 7)).And + .Contain(('h', 'd', 4)).And + .HaveCount(7); + } + + [Test] + public void BellmanFordSingleSourceShortestPathNegativeCycle() + { + var run = new MoveTypeEdgeData { moveType = MoveType.Run }; + var jump = new MoveTypeEdgeData { moveType = MoveType.Jump }; + var wallJump = new MoveTypeEdgeData { moveType = MoveType.WallJump }; + + var myGraph = new DirectedWeightedGraph(); + myGraph.AddNodes('a', 'b', 'c'); + myGraph.AddEdges( + ('a', 'b', -2, run), + ('b', 'c', -3, run), + ('c', 'a', -1, jump) + ); + + myGraph + .Invoking(x => x.BellmanFordSingleSourceShortestPath('a').Count()) + .Should(). + Throw(); + } + + [Test] + public void BellmanFordShortestPath() + { + var run = new MoveTypeEdgeData { moveType = MoveType.Run }; + var jump = new MoveTypeEdgeData { moveType = MoveType.Jump }; + var wallJump = new MoveTypeEdgeData { moveType = MoveType.WallJump }; + + var myGraph = new DirectedWeightedGraph(); + myGraph.AddNodes('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'); + myGraph.AddEdges( + ('a', 'b', 2, run), + ('a', 'c', 3, run), + ('a', 'e', 4, wallJump), + ('b', 'd', 2, jump), + ('b', 'e', 1, run), + ('c', 'g', 4, jump), + ('c', 'h', 11, run), + ('d', 'c', 3, jump), + ('d', 'f', 2, run), + ('d', 'h', 3, wallJump), + ('e', 'f', 5, run), + ('f', 'd', 2, run), + ('f', 'h', 6, wallJump), + ('g', 'h', 7, run), + ('h', 'f', 1, jump) + ); + + myGraph + .BellmanFordShortestPath('a', 'h') + .Select(pair => myGraph.EdgeData(pair.Item1, pair.Item2)) + .Should() + .ContainInOrder( + run, jump, wallJump + ) + .And + .HaveCount(3); + + // have to call Count() because otherwise the lazy evaluation wont trigger + myGraph.Invoking(x => x.BellmanFordShortestPath('a', 'z').Count()).Should().Throw(); + } + + [Test] + public void BellmanFordNotReachable() + { + var run = new MoveTypeEdgeData { moveType = MoveType.Run }; + var jump = new MoveTypeEdgeData { moveType = MoveType.Jump }; + var wallJump = new MoveTypeEdgeData { moveType = MoveType.WallJump }; + + var myGraph = new DirectedWeightedGraph(); + myGraph.AddNodes('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'); + myGraph.AddEdges( + ('a', 'b', 2, run), + ('a', 'c', 3, run), + ('a', 'e', 4, wallJump), + ('b', 'd', 2, jump), + ('b', 'e', 1, run), + ('c', 'g', 4, jump), + ('c', 'h', 11, run), + ('d', 'c', 3, jump), + ('d', 'h', 3, wallJump), + ('f', 'd', 2, run), + ('f', 'h', 6, wallJump), + ('g', 'h', 7, run) + ); + + myGraph + .BellmanFordShortestPath('a', 'f') + .Should() + .BeEmpty(); + } } } \ No newline at end of file