From 1740679052fe5fcac70d7cd5fae77d74d3319614 Mon Sep 17 00:00:00 2001 From: Evan Hemsley Date: Wed, 23 Oct 2019 21:29:26 -0700 Subject: [PATCH] undirected weighted graph --- Graph/DirectedWeightedGraph.cs | 4 +- Graph/SimpleGraph.cs | 1 + Graph/UndirectedWeightedGraph.cs | 19 ++ README.md | 1 + test/DirectedGraph.cs | 2 + test/UndirectedWeightedGraph.cs | 357 +++++++++++++++++++++++++++++++ 6 files changed, 382 insertions(+), 2 deletions(-) create mode 100644 Graph/UndirectedWeightedGraph.cs create mode 100644 test/UndirectedWeightedGraph.cs diff --git a/Graph/DirectedWeightedGraph.cs b/Graph/DirectedWeightedGraph.cs index bbc6ec2..812fc36 100644 --- a/Graph/DirectedWeightedGraph.cs +++ b/Graph/DirectedWeightedGraph.cs @@ -10,13 +10,13 @@ namespace MoonTools.Core.Graph { protected Dictionary<(TNode, TNode), int> weights = new Dictionary<(TNode, TNode), int>(); - public void AddEdge(TNode v, TNode u, int weight, TEdgeData edgeData) + public virtual void AddEdge(TNode v, TNode u, int weight, TEdgeData edgeData) { BaseAddEdge(v, u, edgeData); weights.Add((v, u), weight); } - public void AddEdges(params (TNode, TNode, int weight, TEdgeData)[] edges) + public virtual void AddEdges(params (TNode, TNode, int weight, TEdgeData)[] edges) { foreach (var edge in edges) { diff --git a/Graph/SimpleGraph.cs b/Graph/SimpleGraph.cs index bbb41fb..28e639f 100644 --- a/Graph/SimpleGraph.cs +++ b/Graph/SimpleGraph.cs @@ -15,6 +15,7 @@ namespace MoonTools.Core.Graph protected void BaseAddEdge(TNode v, TNode u, TEdgeData edgeData) { CheckNodes(v, u); + if (Exists(v, u)) { throw new ArgumentException($"Edge between {v} and {u} already exists in the graph"); } if (v.Equals(u)) { throw new ArgumentException("Self-edges are not allowed in a simple graph. Use a multigraph instead"); } diff --git a/Graph/UndirectedWeightedGraph.cs b/Graph/UndirectedWeightedGraph.cs new file mode 100644 index 0000000..a8f89e3 --- /dev/null +++ b/Graph/UndirectedWeightedGraph.cs @@ -0,0 +1,19 @@ +namespace MoonTools.Core.Graph +{ + public class UndirectedWeightedGraph : DirectedWeightedGraph where TNode : System.IEquatable + { + public override void AddEdge(TNode v, TNode u, int weight, TEdgeData edgeData) + { + base.AddEdge(v, u, weight, edgeData); + base.AddEdge(u, v, weight, edgeData); + } + + public override void AddEdges(params (TNode, TNode, int, TEdgeData)[] edges) + { + foreach (var edge in edges) + { + AddEdge(edge.Item1, edge.Item2, edge.Item3, edge.Item4); + } + } + } +} \ No newline at end of file diff --git a/README.md b/README.md index cb13097..784157e 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ A GC-friendly graph theory library for C# intended for use with games. * Directed Weighted * Directed Weighted Multigraph * Undirected +* Undirected Weighted ## Notes diff --git a/test/DirectedGraph.cs b/test/DirectedGraph.cs index 420826a..830fae5 100644 --- a/test/DirectedGraph.cs +++ b/test/DirectedGraph.cs @@ -40,6 +40,8 @@ namespace Tests myGraph.AddEdge(5, 6, dummyEdgeData); Assert.That(myGraph.Neighbors(5), Does.Contain(6)); + + myGraph.Invoking(x => x.AddEdge(5, 6, dummyEdgeData)).Should().Throw(); } [Test] diff --git a/test/UndirectedWeightedGraph.cs b/test/UndirectedWeightedGraph.cs new file mode 100644 index 0000000..d0c93be --- /dev/null +++ b/test/UndirectedWeightedGraph.cs @@ -0,0 +1,357 @@ +using NUnit.Framework; +using FluentAssertions; + +using MoonTools.Core.Graph; +using System; +using System.Linq; + +namespace Tests +{ + public class UndirectedWeightedGraphTests + { + EdgeData dummyEdgeData; + + [Test] + public void Clear() + { + var myGraph = new UndirectedWeightedGraph(); + 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 EdgeExists() + { + var myGraph = new UndirectedWeightedGraph(); + myGraph.AddNodes(1, 2, 3); + myGraph.AddEdges( + (1, 2, 4, dummyEdgeData), + (2, 3, 5, dummyEdgeData) + ); + + myGraph.Exists(1, 2).Should().BeTrue(); + myGraph.Exists(2, 1).Should().BeTrue(); + myGraph.Exists(2, 3).Should().BeTrue(); + myGraph.Exists(3, 2).Should().BeTrue(); + myGraph.Exists(1, 3).Should().BeFalse(); + myGraph.Invoking(x => x.Exists(3, 4)).Should().Throw(); + } + + [Test] + public void Neighbors() + { + var myGraph = new UndirectedWeightedGraph(); + 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(1); + myGraph.Neighbors(2).Should().Contain(3); + myGraph.Neighbors(3).Should().Contain(2); + myGraph.Neighbors(1).Should().NotContain(3); + myGraph.Invoking(x => x.Neighbors(4)).Should().Throw(); + } + + [Test] + public void Weight() + { + var myGraph = new UndirectedWeightedGraph(); + 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, 1).Should().Be(4); + myGraph.Weight(2, 3).Should().Be(5); + myGraph.Weight(3, 2).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 UndirectedWeightedGraph(); + 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, 1).Should().Be(a); + myGraph.EdgeData(2, 3).Should().Be(b); + myGraph.EdgeData(3, 2).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 UndirectedWeightedGraph(); + 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', 'h', 6, wallJump), + ('g', 'h', 7, run) + ); + + myGraph + .AStarShortestPath('a', 'h', (x, y) => 1) + .Select(edge => myGraph.EdgeData(edge.Item1, edge.Item2)) + .Should() + .ContainInOrder( + run, jump, wallJump + ) + .And + .HaveCount(3); + + // 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 DijkstraSingleSourceShortestPath() + { + var run = new MoveTypeEdgeData { moveType = MoveType.Run }; + var jump = new MoveTypeEdgeData { moveType = MoveType.Jump }; + var wallJump = new MoveTypeEdgeData { moveType = MoveType.WallJump }; + + var myGraph = new UndirectedWeightedGraph(); + 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', 'h', 6, wallJump), + ('g', 'h', 7, run) + ); + + myGraph + .DijkstraSingleSourceShortestPath('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.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 UndirectedWeightedGraph(); + 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', 'h', 6, wallJump), + ('g', 'h', 7, run) + ); + + 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 UndirectedWeightedGraph(); + myGraph.AddNodes('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'); + myGraph.AddEdges( + ('a', 'b', 2, run), + ('a', 'e', 4, wallJump), + ('b', 'e', 1, run), + ('c', 'g', 4, 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 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 UndirectedWeightedGraph(); + 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', 'h', 6, wallJump), + ('g', 'h', 7, run) + ); + + 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 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 UndirectedWeightedGraph(); + 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', 'h', 6, wallJump), + ('g', 'h', 7, run) + ); + + 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', 'e', 4, wallJump), + ('b', 'e', 1, run), + ('c', 'g', 4, 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