From 9e8bc14a40c1c7599de8374de01851f437266df1 Mon Sep 17 00:00:00 2001 From: Evan Hemsley Date: Wed, 23 Oct 2019 22:33:19 -0700 Subject: [PATCH] directed weighted multigraph SSSP --- Graph/DirectedWeightedMultiGraph.cs | 152 +++++++++++++++++- Graph/MultiGraph.cs | 22 ++- README.md | 2 +- test/DirectedWeightedMultiGraph.cs | 236 ++++++++++++++++++++++++++++ 4 files changed, 405 insertions(+), 7 deletions(-) diff --git a/Graph/DirectedWeightedMultiGraph.cs b/Graph/DirectedWeightedMultiGraph.cs index 06974fe..cbced37 100644 --- a/Graph/DirectedWeightedMultiGraph.cs +++ b/Graph/DirectedWeightedMultiGraph.cs @@ -34,7 +34,13 @@ namespace MoonTools.Core.Graph public IEnumerable Weights(TNode v, TNode u) { CheckNodes(v, u); - return edges[(v, u)].Select(id => weights[id]); + return edges[(v, u)].Select(id => Weight(id)); + } + + public int Weight(Guid id) + { + if (!IDToEdge.ContainsKey(id)) { throw new ArgumentException($"Edge with id {id} does not exist in the graph"); } + return weights[id]; } private IEnumerable ReconstructPath(PooledDictionary cameFrom, TNode currentNode) @@ -115,5 +121,149 @@ namespace MoonTools.Core.Graph yield break; } + + private IEnumerable 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, Guid, 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); + var previousEdgeIDs = new PooledDictionary(ClearMode.Always); + + foreach (var node in Nodes) + { + distance[node] = int.MaxValue; + } + + distance[source] = 0; + + var q = Nodes.ToPooledList(); + + while (q.Count > 0) + { + var node = q.MinBy(n => distance[n]).First(); + q.Remove(node); + if (distance[node] == int.MaxValue) { break; } + + foreach (var neighbor in Neighbors(node)) + { + foreach (var edgeID in EdgeIDs(node, neighbor)) + { + var weight = Weight(edgeID); + + var alt = distance[node] + weight; + if (alt < distance[neighbor]) + { + distance[neighbor] = alt; + previousEdgeIDs[neighbor] = edgeID; + } + } + } + } + + foreach (var node in Nodes) + { + if (previousEdgeIDs.ContainsKey(node) && distance.ContainsKey(node)) + { + yield return (node, previousEdgeIDs[node], distance[node]); + } + } + + distance.Dispose(); + previousEdgeIDs.Dispose(); + } + + public IEnumerable DijkstraShortestPath(TNode start, TNode end) + { + return ShortestPath(start, end, DijkstraSingleSourceShortestPath); + } + + public IEnumerable<(TNode, Guid, 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 edgeID in IDToEdge.Keys) + { + var weight = Weight(edgeID); + var (v, u) = IDToEdge[edgeID]; + + if (distance[v] + weight < distance[u]) + { + distance[u] = distance[v] + weight; + previous[u] = edgeID; + } + } + } + + foreach (var edgeID in IDToEdge.Keys) + { + var (v, u) = IDToEdge[edgeID]; + + foreach (var weight in Weights(v, u)) + { + if (distance[v] + weight < 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 BellmanFordShortestPath(TNode start, TNode end) + { + return ShortestPath(start, end, BellmanFordSingleSourceShortestPath); + } } } \ No newline at end of file diff --git a/Graph/MultiGraph.cs b/Graph/MultiGraph.cs index 0449886..b37fa6d 100644 --- a/Graph/MultiGraph.cs +++ b/Graph/MultiGraph.cs @@ -47,13 +47,25 @@ namespace MoonTools.Core.Graph return edges.ContainsKey((v, u)); } + private void CheckID(Guid id) + { + if (!Exists(id)) { throw new ArgumentException($"Edge {id} does not exist in the graph."); } + } + + public bool Exists(Guid id) + { + return IDToEdge.ContainsKey(id); + } + + public (TNode, TNode) EdgeNodes(Guid id) + { + CheckID(id); + return IDToEdge[id]; + } + public TEdgeData EdgeData(Guid id) { - if (!edgeToEdgeData.ContainsKey(id)) - { - throw new ArgumentException($"Edge {id} does not exist in the graph."); - } - + CheckID(id); return edgeToEdgeData[id]; } } diff --git a/README.md b/README.md index 784157e..df157c5 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ A GC-friendly graph theory library for C# intended for use with games. ## Usage -`Graph` implements the following graph structures: +`Graph` implements various algorithms on the following graph structures: * Directed * Directed Weighted diff --git a/test/DirectedWeightedMultiGraph.cs b/test/DirectedWeightedMultiGraph.cs index 1caed31..699c17c 100644 --- a/test/DirectedWeightedMultiGraph.cs +++ b/test/DirectedWeightedMultiGraph.cs @@ -252,5 +252,241 @@ namespace Tests // have to call Count() because otherwise the lazy evaluation wont trigger myGraph.Invoking(x => x.AStarShortestPath('a', 'z', (x, y) => 1).Count()).Should().Throw(); } + + [Test] + public void DijsktraSingleSourceShortestPath() + { + var run = new MoveTypeEdgeData { moveType = MoveType.Run }; + var jump = new MoveTypeEdgeData { moveType = MoveType.Jump }; + var wallJump = new MoveTypeEdgeData { moveType = MoveType.WallJump }; + + var myGraph = new DirectedWeightedMultiGraph(); + myGraph.AddNodes('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'); + + var edgeA = myGraph.AddEdge('a', 'b', 2, run); + var edgeB = myGraph.AddEdge('a', 'c', 1, jump); + var edgeC = myGraph.AddEdge('b', 'd', 2, jump); + var edgeD = myGraph.AddEdge('b', 'e', 1, run); + var edgeE = myGraph.AddEdge('d', 'f', 2, run); + var edgeF = myGraph.AddEdge('c', 'g', 2, run); + var edgeG = myGraph.AddEdge('d', 'h', 3, wallJump); + + myGraph.AddEdges( + ('a', 'c', 3, run), + ('a', 'e', 4, wallJump), + ('b', 'd', 5, run), + ('c', 'g', 4, jump), + ('c', 'h', 11, run), + ('d', 'c', 3, jump), + ('e', 'f', 5, run), + ('f', 'd', 2, run), + ('f', 'h', 6, wallJump), + ('g', 'h', 7, run), + ('h', 'f', 1, jump), + ('a', 'a', 3, jump) // cheeky lil self-edge + ); + + myGraph + .DijkstraSingleSourceShortestPath('a') + .Should() + .Contain(('b', edgeA, 2)).And + .Contain(('c', edgeB, 1)).And + .Contain(('d', edgeC, 4)).And + .Contain(('e', edgeD, 3)).And + .Contain(('f', edgeE, 6)).And + .Contain(('g', edgeF, 3)).And + .Contain(('h', edgeG, 7)).And + .HaveCount(7); + + // 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 DirectedWeightedMultiGraph(); + myGraph.AddNodes('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'); + + var edgeA = myGraph.AddEdge('a', 'b', 2, run); + var edgeB = myGraph.AddEdge('a', 'c', 1, jump); + var edgeC = myGraph.AddEdge('b', 'd', 2, jump); + var edgeD = myGraph.AddEdge('b', 'e', 1, run); + var edgeE = myGraph.AddEdge('d', 'f', 2, run); + var edgeF = myGraph.AddEdge('c', 'g', 2, run); + var edgeG = myGraph.AddEdge('d', 'h', 3, wallJump); + + myGraph.AddEdges( + ('a', 'c', 3, run), + ('a', 'e', 4, wallJump), + ('b', 'd', 5, run), + ('c', 'g', 4, jump), + ('c', 'h', 11, run), + ('d', 'c', 3, jump), + ('e', 'f', 5, run), + ('f', 'd', 2, run), + ('f', 'h', 6, wallJump), + ('g', 'h', 7, run), + ('h', 'f', 1, jump), + ('a', 'a', 3, jump) // cheeky lil self-edge + ); + + myGraph + .DijkstraShortestPath('a', 'h') + .Select(edgeID => myGraph.EdgeData(edgeID)) + .Should() + .ContainInOrder( + run, jump, wallJump + ) + .And + .HaveCount(3); + + myGraph.Invoking(x => x.DijkstraShortestPath('a', 'z').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 DirectedWeightedMultiGraph(); + myGraph.AddNodes('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'); + + var edgeA = myGraph.AddEdge('a', 'b', 2, run); + var edgeB = myGraph.AddEdge('a', 'c', 1, jump); + var edgeC = myGraph.AddEdge('b', 'd', 2, jump); + var edgeD = myGraph.AddEdge('b', 'e', 1, run); + var edgeE = myGraph.AddEdge('d', 'f', 2, run); + var edgeF = myGraph.AddEdge('c', 'g', 2, run); + var edgeG = myGraph.AddEdge('d', 'h', 3, wallJump); + + myGraph.AddEdges( + ('a', 'c', 3, run), + ('a', 'e', 4, wallJump), + ('b', 'd', 5, run), + ('c', 'g', 4, jump), + ('c', 'h', 11, run), + ('d', 'c', 3, jump), + ('e', 'f', 5, run), + ('f', 'd', 2, run), + ('f', 'h', 6, wallJump), + ('g', 'h', 7, run), + ('h', 'f', 1, jump), + ('a', 'a', 3, jump) // cheeky lil self-edge + ); + + myGraph + .BellmanFordSingleSourceShortestPath('a') + .Should() + .Contain(('b', edgeA, 2)).And + .Contain(('c', edgeB, 1)).And + .Contain(('d', edgeC, 4)).And + .Contain(('e', edgeD, 3)).And + .Contain(('f', edgeE, 6)).And + .Contain(('g', edgeF, 3)).And + .Contain(('h', edgeG, 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 BellmanFordSingleSourceShortestPathNegative() + { + var run = new MoveTypeEdgeData { moveType = MoveType.Run }; + var jump = new MoveTypeEdgeData { moveType = MoveType.Jump }; + var wallJump = new MoveTypeEdgeData { moveType = MoveType.WallJump }; + + var myGraph = new DirectedWeightedMultiGraph(); + myGraph.AddNodes('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'); + + var edgeA = myGraph.AddEdge('a', 'b', 2, run); + var edgeB = myGraph.AddEdge('a', 'c', 1, jump); + var edgeC = myGraph.AddEdge('b', 'd', -1, jump); + var edgeD = myGraph.AddEdge('b', 'e', 1, run); + var edgeE = myGraph.AddEdge('d', 'f', 2, run); + var edgeF = myGraph.AddEdge('c', 'g', 2, run); + var edgeG = myGraph.AddEdge('d', 'h', 3, wallJump); + + myGraph.AddEdges( + ('a', 'c', 3, run), + ('a', 'e', 4, wallJump), + ('b', 'd', 5, run), + ('c', 'g', 4, jump), + ('c', 'h', 11, run), + ('d', 'c', 3, jump), + ('e', 'f', 5, run), + ('f', 'd', 2, run), + ('f', 'h', 6, wallJump), + ('g', 'h', 7, run), + ('h', 'f', 1, jump), + ('a', 'a', 3, jump) // cheeky lil self-edge + ); + + myGraph + .BellmanFordSingleSourceShortestPath('a') + .Should() + .Contain(('b', edgeA, 2)).And + .Contain(('c', edgeB, 1)).And + .Contain(('d', edgeC, 1)).And + .Contain(('e', edgeD, 3)).And + .Contain(('f', edgeE, 3)).And + .Contain(('g', edgeF, 3)).And + .Contain(('h', edgeG, 4)).And + .HaveCount(7); + } + + [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 DirectedWeightedMultiGraph(); + myGraph.AddNodes('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'); + + var edgeA = myGraph.AddEdge('a', 'b', 2, run); + var edgeB = myGraph.AddEdge('a', 'c', 1, jump); + var edgeC = myGraph.AddEdge('b', 'd', 2, jump); + var edgeD = myGraph.AddEdge('b', 'e', 1, run); + var edgeE = myGraph.AddEdge('d', 'f', 2, run); + var edgeF = myGraph.AddEdge('c', 'g', 2, run); + var edgeG = myGraph.AddEdge('d', 'h', 3, wallJump); + + myGraph.AddEdges( + ('a', 'c', 3, run), + ('a', 'e', 4, wallJump), + ('b', 'd', 5, run), + ('c', 'g', 4, jump), + ('c', 'h', 11, run), + ('d', 'c', 3, jump), + ('e', 'f', 5, run), + ('f', 'd', 2, run), + ('f', 'h', 6, wallJump), + ('g', 'h', 7, run), + ('h', 'f', 1, jump), + ('a', 'a', 3, jump) // cheeky lil self-edge + ); + + myGraph + .BellmanFordShortestPath('a', 'h') + .Select(edgeID => myGraph.EdgeData(edgeID)) + .Should() + .ContainInOrder( + run, jump, wallJump + ) + .And + .HaveCount(3); + + myGraph.Invoking(x => x.BellmanFordShortestPath('a', 'z').Count()).Should().Throw(); + } } } \ No newline at end of file