From df320194f18ea0a38c51a341a6ef73be4f796289 Mon Sep 17 00:00:00 2001 From: evan Date: Tue, 22 Oct 2019 19:04:00 -0700 Subject: [PATCH] directed weighted graph --- Graph/DirectedWeightedGraph.cs | 169 +++++++++++++++++++++ Graph/DirectedWeightedMultiGraph.cs | 43 ++---- test/DirectedWeightedGraph.cs | 224 ++++++++++++++++++++++++++++ test/DirectedWeightedMultiGraph.cs | 2 +- 4 files changed, 410 insertions(+), 28 deletions(-) create mode 100644 Graph/DirectedWeightedGraph.cs create mode 100644 test/DirectedWeightedGraph.cs diff --git a/Graph/DirectedWeightedGraph.cs b/Graph/DirectedWeightedGraph.cs new file mode 100644 index 0000000..c7b8671 --- /dev/null +++ b/Graph/DirectedWeightedGraph.cs @@ -0,0 +1,169 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using MoreLinq; + +namespace MoonTools.Core.Graph +{ + public class DirectedWeightedGraph : IGraph where TNode : System.IEquatable + { + protected HashSet nodes = new HashSet(); + protected Dictionary> neighbors = new Dictionary>(); + protected Dictionary<(TNode, TNode), TEdgeData> edgeToEdgeData = new Dictionary<(TNode, TNode), TEdgeData>(); + protected Dictionary<(TNode, TNode), int> weights = new Dictionary<(TNode, TNode), int>(); + + // store search sets to prevent GC + protected HashSet openSet = new HashSet(); + protected HashSet closedSet = new HashSet(); + protected Dictionary gScore = new Dictionary(); + protected Dictionary fScore = new Dictionary(); + protected Dictionary cameFrom = new Dictionary(); + + public IEnumerable Nodes => nodes; + + public void AddNode(TNode node) + { + if (Exists(node)) { return; } + + nodes.Add(node); + neighbors[node] = new HashSet(); + } + + public void AddNodes(params TNode[] nodes) + { + foreach (var node in nodes) + { + AddNode(node); + } + } + + private void CheckNodes(params TNode[] givenNodes) + { + foreach (var node in givenNodes) + { + if (!Exists(node)) + { + throw new ArgumentException($"Vertex {node} does not exist in the graph"); + } + } + } + + public void AddEdge(TNode v, TNode u, int weight, TEdgeData data) + { + CheckNodes(v, u); + + if (Exists(v, u)) { throw new ArgumentException($"Edge with vertex {v} and {u} already exists in the graph"); } + + neighbors[v].Add(u); + weights.Add((v, u), weight); + edgeToEdgeData.Add((v, u), data); + } + + public void AddEdges(params (TNode, TNode, int, TEdgeData)[] edges) + { + foreach (var edge in edges) + { + AddEdge(edge.Item1, edge.Item2, edge.Item3, edge.Item4); + } + } + + public void Clear() + { + nodes.Clear(); + neighbors.Clear(); + edgeToEdgeData.Clear(); + weights.Clear(); + } + + public bool Exists(TNode node) => nodes.Contains(node); + + public bool Exists(TNode v, TNode u) + { + CheckNodes(v, u); + return neighbors[v].Contains(u); + } + + public IEnumerable Neighbors(TNode node) + { + CheckNodes(node); + return neighbors[node]; + } + + private void CheckEdge(TNode v, TNode u) + { + CheckNodes(v, u); + if (!Exists(v, u)) { throw new ArgumentException($"Edge between vertex {v} and vertex {u} does not exist in the graph"); } + } + + public int Weight(TNode v, TNode u) + { + CheckEdge(v, u); + return weights[(v, u)]; + } + + public TEdgeData EdgeData(TNode v, TNode u) + { + CheckEdge(v, u); + return edgeToEdgeData[(v, u)]; + } + + private IEnumerable<(TNode, TNode)> ReconstructPath(Dictionary cameFrom, TNode currentNode) + { + while (cameFrom.ContainsKey(currentNode)) + { + var edge = (cameFrom[currentNode], currentNode); + currentNode = edge.Item1; + yield return edge; + } + } + + public IEnumerable<(TNode, TNode)> AStarShortestPath(TNode start, TNode end, Func heuristic) + { + CheckNodes(start, end); + + openSet.Clear(); + closedSet.Clear(); + gScore.Clear(); + fScore.Clear(); + cameFrom.Clear(); + + openSet.Add(start); + + gScore[start] = 0; + fScore[start] = heuristic(start, end); + + while (openSet.Count > 0) + { + var currentNode = openSet.MinBy(node => fScore[node]).First(); + + if (currentNode.Equals(end)) + { + return ReconstructPath(cameFrom, currentNode).Reverse(); + } + + openSet.Remove(currentNode); + closedSet.Add(currentNode); + + foreach (var neighbor in Neighbors(currentNode)) + { + if (!closedSet.Contains(neighbor)) + { + var weight = weights[(currentNode, neighbor)]; + + var tentativeGScore = gScore.ContainsKey(currentNode) ? gScore[currentNode] + weight : int.MaxValue; + + if (!openSet.Contains(neighbor) || tentativeGScore < gScore[neighbor]) + { + cameFrom[neighbor] = currentNode; + gScore[neighbor] = tentativeGScore; + fScore[neighbor] = tentativeGScore + heuristic(neighbor, end); + openSet.Add(neighbor); + } + } + } + } + + return Enumerable.Empty<(TNode, TNode)>(); + } + } +} \ No newline at end of file diff --git a/Graph/DirectedWeightedMultiGraph.cs b/Graph/DirectedWeightedMultiGraph.cs index 839ccee..9cb4a11 100644 --- a/Graph/DirectedWeightedMultiGraph.cs +++ b/Graph/DirectedWeightedMultiGraph.cs @@ -25,7 +25,10 @@ namespace MoonTools.Core.Graph public void AddNode(TNode node) { + if (Exists(node)) { return; } + nodes.Add(node); + neighbors[node] = new HashSet(); } public void AddNodes(params TNode[] nodes) @@ -38,31 +41,18 @@ namespace MoonTools.Core.Graph public void AddEdge(TNode v, TNode u, int weight, TEdgeData data) { - if (Exists(v) && Exists(u)) + CheckNodes(v, u); + + var id = Guid.NewGuid(); + neighbors[v].Add(u); + weights.Add(id, weight); + if (!edges.ContainsKey((v, u))) { - var id = Guid.NewGuid(); - if (!neighbors.ContainsKey(v)) - { - neighbors[v] = new HashSet(); - } - neighbors[v].Add(u); - weights.Add(id, weight); - if (!edges.ContainsKey((v, u))) - { - edges[(v, u)] = new HashSet(); - } - edges[(v, u)].Add(id); - edgeToEdgeData.Add(id, data); - IDToEdge.Add(id, (v, u)); - } - else if (!Exists(v)) - { - throw new InvalidVertexException("Vertex {0} does not exist in the graph", v); - } - else - { - throw new InvalidVertexException("Vertex {0} does not exist in the graph", u); + edges[(v, u)] = new HashSet(); } + edges[(v, u)].Add(id); + edgeToEdgeData.Add(id, data); + IDToEdge.Add(id, (v, u)); } public void AddEdges(params (TNode, TNode, int, TEdgeData)[] edges) @@ -78,6 +68,8 @@ namespace MoonTools.Core.Graph nodes.Clear(); neighbors.Clear(); weights.Clear(); + edges.Clear(); + IDToEdge.Clear(); edgeToEdgeData.Clear(); } @@ -98,10 +90,7 @@ namespace MoonTools.Core.Graph return edges.ContainsKey((v, u)) ? edges[(v, u)] : Enumerable.Empty(); } - public bool Exists(TNode node) - { - return nodes.Contains(node); - } + public bool Exists(TNode node) => nodes.Contains(node); public bool Exists(TNode v, TNode u) { diff --git a/test/DirectedWeightedGraph.cs b/test/DirectedWeightedGraph.cs new file mode 100644 index 0000000..887e582 --- /dev/null +++ b/test/DirectedWeightedGraph.cs @@ -0,0 +1,224 @@ +using NUnit.Framework; +using FluentAssertions; + +using MoonTools.Core.Graph; +using System; +using System.Linq; + +namespace Tests +{ + public class DirectedWeightedGraphTests + { + EdgeData dummyEdgeData; + + [Test] + public void AddNode() + { + var myGraph = new DirectedWeightedGraph(); + myGraph.AddNode(4); + + myGraph.Exists(4).Should().BeTrue(); + } + + [Test] + public void AddNodes() + { + var myGraph = new DirectedWeightedGraph(); + myGraph.AddNodes(4, 20, 69); + + myGraph.Exists(4).Should().BeTrue(); + myGraph.Exists(20).Should().BeTrue(); + myGraph.Exists(69).Should().BeTrue(); + } + + [Test] + public void AddEdge() + { + var myGraph = new DirectedWeightedGraph(); + myGraph.AddNodes(5, 6); + myGraph.AddEdge(5, 6, 10, dummyEdgeData); + + myGraph.Neighbors(5).Should().Contain(6); + myGraph.Weight(5, 6).Should().Be(10); + myGraph.EdgeData(5, 6).Should().Be(dummyEdgeData); + + myGraph.Invoking(x => x.AddEdge(5, 6, 3, dummyEdgeData)).Should().Throw(); + } + + [Test] + public void AddEdges() + { + var a = new NumEdgeData { testNum = 1 }; + var b = new NumEdgeData { testNum = 2 }; + var c = new NumEdgeData { testNum = 3 }; + var d = new NumEdgeData { testNum = 4 }; + + var myGraph = new DirectedWeightedGraph(); + myGraph.AddNodes(1, 2, 3, 4); + myGraph.AddEdges( + (1, 2, 5, a), + (2, 3, 6, b), + (2, 4, 7, c), + (3, 4, 8, d) + ); + + myGraph.Neighbors(1).Should().Contain(2); + myGraph.Neighbors(2).Should().Contain(3); + myGraph.Neighbors(2).Should().Contain(4); + myGraph.Neighbors(3).Should().Contain(4); + myGraph.Neighbors(1).Should().NotContain(4); + + myGraph.Weight(1, 2).Should().Be(5); + myGraph.Weight(2, 3).Should().Be(6); + myGraph.Weight(2, 4).Should().Be(7); + myGraph.Weight(3, 4).Should().Be(8); + + myGraph.EdgeData(1, 2).Should().Be(a); + myGraph.EdgeData(2, 3).Should().Be(b); + myGraph.EdgeData(2, 4).Should().Be(c); + myGraph.EdgeData(3, 4).Should().Be(d); + + myGraph.Invoking(x => x.AddEdge(2, 4, 9, d)).Should().Throw(); + } + + [Test] + public void Clear() + { + var myGraph = new DirectedWeightedGraph(); + myGraph.AddNodes(1, 2, 3, 4); + myGraph.AddEdges( + (1, 2, 5, dummyEdgeData), + (2, 3, 6, dummyEdgeData), + (2, 4, 7, dummyEdgeData) + ); + + myGraph.Clear(); + + myGraph.Nodes.Should().BeEmpty(); + myGraph.Invoking(x => x.Neighbors(1)).Should().Throw(); + myGraph.Invoking(x => x.Weight(1, 2)).Should().Throw(); + myGraph.Invoking(x => x.EdgeData(1, 2)).Should().Throw(); + } + + [Test] + public void NodeExists() + { + var myGraph = new DirectedWeightedGraph(); + myGraph.AddNodes(1, 2, 3); + myGraph.AddEdges( + (1, 2, 4, dummyEdgeData), + (2, 3, 5, dummyEdgeData) + ); + + myGraph.Exists(1).Should().BeTrue(); + myGraph.Exists(2).Should().BeTrue(); + myGraph.Exists(3).Should().BeTrue(); + myGraph.Exists(4).Should().BeFalse(); + } + + [Test] + public void EdgeExists() + { + var myGraph = new DirectedWeightedGraph(); + myGraph.AddNodes(1, 2, 3); + myGraph.AddEdges( + (1, 2, 4, dummyEdgeData), + (2, 3, 5, dummyEdgeData) + ); + + myGraph.Exists(1, 2).Should().BeTrue(); + myGraph.Exists(2, 3).Should().BeTrue(); + myGraph.Exists(1, 3).Should().BeFalse(); + myGraph.Invoking(x => x.Exists(3, 4)).Should().Throw(); + } + + [Test] + public void Neighbors() + { + var myGraph = new DirectedWeightedGraph(); + myGraph.AddNodes(1, 2, 3); + myGraph.AddEdges( + (1, 2, 4, dummyEdgeData), + (2, 3, 5, dummyEdgeData) + ); + + myGraph.Neighbors(1).Should().Contain(2); + myGraph.Neighbors(2).Should().Contain(3); + myGraph.Neighbors(1).Should().NotContain(3); + myGraph.Invoking(x => x.Neighbors(4)).Should().Throw(); + } + + [Test] + public void Weight() + { + var myGraph = new DirectedWeightedGraph(); + myGraph.AddNodes(1, 2, 3); + myGraph.AddEdges( + (1, 2, 4, dummyEdgeData), + (2, 3, 5, dummyEdgeData) + ); + + myGraph.Weight(1, 2).Should().Be(4); + myGraph.Weight(2, 3).Should().Be(5); + myGraph.Invoking(x => x.Weight(3, 4)).Should().Throw(); + } + + [Test] + public void EdgeData() + { + var a = new NumEdgeData { testNum = 3 }; + var b = new NumEdgeData { testNum = 5 }; + + var myGraph = new DirectedWeightedGraph(); + myGraph.AddNodes(1, 2, 3); + myGraph.AddEdges( + (1, 2, 4, a), + (2, 3, 5, b) + ); + + myGraph.EdgeData(1, 2).Should().Be(a); + myGraph.EdgeData(2, 3).Should().Be(b); + myGraph.Invoking(x => x.EdgeData(2, 4)).Should().Throw(); + } + + [Test] + public void AStarShortestPath() + { + 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 + .AStarShortestPath('a', 'h', (x, y) => 1) + .Select(edge => myGraph.EdgeData(edge.Item1, edge.Item2)) + .Should() + .ContainInOrder( + run, jump, wallJump + ) + .And + .HaveCount(3); + + myGraph.Invoking(x => x.AStarShortestPath('a', 'z', (x, y) => 15)).Should().Throw(); + } + } +} \ No newline at end of file diff --git a/test/DirectedWeightedMultiGraph.cs b/test/DirectedWeightedMultiGraph.cs index a11dfd0..06430da 100644 --- a/test/DirectedWeightedMultiGraph.cs +++ b/test/DirectedWeightedMultiGraph.cs @@ -239,7 +239,7 @@ namespace Tests ); myGraph - .AStarShortestPath('a', 'h', (x, y) => 15) + .AStarShortestPath('a', 'h', (x, y) => 1) .Select(id => myGraph.EdgeData(id)) .Should() .ContainInOrder(