diff --git a/Graph/Edge.cs b/Graph/Edge.cs new file mode 100644 index 0000000..df373c9 --- /dev/null +++ b/Graph/Edge.cs @@ -0,0 +1,14 @@ +namespace Graph +{ + public class Edge + { + public IVertex Destination; + public double Cost; + + public Edge(IVertex destination, double cost) + { + Destination = destination; + Cost = cost; + } + } +} \ No newline at end of file diff --git a/Graph/Graph.cs b/Graph/Graph.cs new file mode 100644 index 0000000..f9ebee3 --- /dev/null +++ b/Graph/Graph.cs @@ -0,0 +1,201 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http.Headers; +using PriorityQueue; + +namespace Graph +{ + public class Graph : IGraph + { + public const double Infinity = double.MaxValue; + + protected Dictionary VertexMap = new Dictionary(); + + /// + /// Try to get the vertex by vertex name, if it does not exist create a + /// new one and add it to the vertex map + /// + /// + /// + public IVertex GetVertex(string vertexName) + { + if (VertexMap.TryGetValue(vertexName, out var vertex)) return vertex; + + vertex = new Vertex(vertexName); + VertexMap.Add(vertexName, vertex); + + return vertex; + } + + public void AddEdge(string sourceVertexName, string destinationVertexName, double cost) + { + var sourceVertex = (Vertex) GetVertex(sourceVertexName); + var destinationVertex = GetVertex(destinationVertexName); + + sourceVertex.AdjacentVertices.Add(new Edge(destinationVertex, cost)); + } + + /// + /// Initializes the vertex output info prior to running any shortest path algorithm + /// + protected void ClearAll() + { + foreach (var vertex in VertexMap.Values) + vertex.Reset(); + } + + /// + /// Method used to print vertices in a path + /// + /// + /// + protected string PathToString(Vertex destination) + { + var result = ""; + + if (destination.PreviousVertex != null) + result += " to " + PathToString(destination.PreviousVertex); + + return destination.Name + result; + } + + /// + /// Method used to print a path + /// + /// + /// + /// + public string PathToString(string destinationName) + { + if (!VertexMap.TryGetValue(destinationName, out var vertex)) + throw new NoSuchElementException(); + + if (vertex.Distance == Infinity) + return destinationName + " is unreachable"; + + return $"(Cost is: {vertex.Distance}) {PathToString(vertex)}"; + } + + + public override string ToString() + { + throw new NotImplementedException(); + } + + #region Shortest-path algorithms + + /// + /// Single source unweighted shortest-path algorithm + /// + /// + /// + public void Unweighted(string startVertexName) + { + ClearAll(); + + if (!VertexMap.TryGetValue(startVertexName, out var start)) + throw new NoSuchElementException(); + + var queue = new Queue(); + + // Add start vertex to queue + queue.Enqueue(start); + start.Distance = 0; + + while (queue.Count != 0) + { + var currentVertex = queue.Dequeue(); + + // Set visited to true so we can check if the graph is connected or not + currentVertex.Visited = true; + + currentVertex.AdjacentVertices.ForEach(edge => + { + var adjacentVertex = (Vertex) edge.Destination; + + if (adjacentVertex.Distance != Infinity) return; + + adjacentVertex.Distance = currentVertex.Distance + 1; + adjacentVertex.PreviousVertex = currentVertex; + + queue.Enqueue(adjacentVertex); + }); + } + } + + public void Dijkstra(string startVertexName) + { + ClearAll(); + + if (!VertexMap.TryGetValue(startVertexName, out var start)) + throw new NoSuchElementException(); + + var priorityQueue = new PriorityQueue(); + + // Add start path to priority queue + priorityQueue.Enqueue(new Path(start, 0)); + start.Distance = 0; + + var nodesSeen = 0; + while (!priorityQueue.IsEmpty() && nodesSeen < VertexMap.Count) + { + var vertexRecord = priorityQueue.Dequeue(); + var vertex = vertexRecord.Destination; + + // Don't revisit vertex + if (vertex.Visited) continue; + + vertex.Visited = true; + nodesSeen++; + + vertex.AdjacentVertices.ForEach(edge => + { + var adjacentVertex = (Vertex) edge.Destination; + var edgeCost = edge.Cost; + + if (edgeCost < 0) + throw new GraphException("Graph has negative edges"); + + // Don't update the distance of to the adjacent vertex when the distance is higher + if (!(vertex.Distance + edgeCost < adjacentVertex.Distance)) + return; + + adjacentVertex.Distance = vertex.Distance + edgeCost; + adjacentVertex.PreviousVertex = vertex; + + priorityQueue.Enqueue(new Path(adjacentVertex, adjacentVertex.Distance)); + }); + } + } + + #endregion + + public bool IsConnected() + { + Unweighted(VertexMap.First().Value.Name); + + foreach (var vertex in VertexMap.Values) + // Return false when any vertex has not been visited + if (!vertex.Visited) + return false; + + return true; + } + } + + #region Custom error classes + + internal class NoSuchElementException : Exception + { + } + + internal class GraphException : Exception + { + internal GraphException(string message) : base(message) + { + } + } + + #endregion +} \ No newline at end of file diff --git a/Graph/Graph.csproj b/Graph/Graph.csproj new file mode 100644 index 0000000..409a5db --- /dev/null +++ b/Graph/Graph.csproj @@ -0,0 +1,11 @@ + + + + netcoreapp2.0 + + + + + + + diff --git a/Graph/IGraph.cs b/Graph/IGraph.cs new file mode 100644 index 0000000..e1d05f7 --- /dev/null +++ b/Graph/IGraph.cs @@ -0,0 +1,11 @@ +namespace Graph +{ + public interface IGraph + { + IVertex GetVertex(string vertexName); + + void AddEdge(string sourceVertexName, string destinationVertexName, double cost); + + string ToString(); + } +} \ No newline at end of file diff --git a/Graph/IVertex.cs b/Graph/IVertex.cs new file mode 100644 index 0000000..b8d46f9 --- /dev/null +++ b/Graph/IVertex.cs @@ -0,0 +1,9 @@ +namespace Graph +{ + public interface IVertex + { + void Reset(); + + string ToString(); + } +} \ No newline at end of file diff --git a/Graph/Path.cs b/Graph/Path.cs new file mode 100644 index 0000000..4c91070 --- /dev/null +++ b/Graph/Path.cs @@ -0,0 +1,32 @@ +using System; + +namespace Graph +{ + public class Path : IComparable + { + public Vertex Destination; + public double Cost; + + public Path(Vertex destination, double cost) + { + Destination = destination; + Cost = cost; + } + + /// + /// Implementation of Comparable interface + /// + /// + /// + public int CompareTo(object other) + { + var path = (Path) other; + + return Cost < path.Cost + ? -1 + : Cost > path.Cost + ? 1 + : 0; + } + } +} \ No newline at end of file diff --git a/Graph/Vertex.cs b/Graph/Vertex.cs new file mode 100644 index 0000000..ca31a2c --- /dev/null +++ b/Graph/Vertex.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; + +namespace Graph +{ + public class Vertex : IVertex + { + public string Name; + public double Distance; + + public List AdjacentVertices = new List(); + public Vertex PreviousVertex; // Previous vertex on *shortest* path + + // Variable used in path finding algorithms + public bool Visited; + + /// + /// + /// + /// + public Vertex(string name) + { + Name = name; + } + + /// + /// + /// + public void Reset() + { + Distance = Graph.Infinity; + PreviousVertex = default(Vertex); + Visited = false; + } + } +} \ No newline at end of file diff --git a/GraphTests/GraphTests.cs b/GraphTests/GraphTests.cs new file mode 100644 index 0000000..d10135a --- /dev/null +++ b/GraphTests/GraphTests.cs @@ -0,0 +1,143 @@ +using System; +using Graph; +using Xunit; +using Xunit.Abstractions; + +namespace GraphTests +{ + public class GraphTests + { + private readonly ITestOutputHelper _output; + + public GraphTests(ITestOutputHelper output) + { + _output = output; + } + + private Graph.Graph CreateGraph() + { + var graph = new Graph.Graph(); + + // V0 + graph.AddEdge("V0", "V1", 2); + graph.AddEdge("V0", "V3", 1); + + // V1 + graph.AddEdge("V1", "V3", 3); + graph.AddEdge("V1", "V4", 10); + + // V2 + graph.AddEdge("V2", "V0", 4); + graph.AddEdge("V2", "V5", 5); + + // V3 + graph.AddEdge("V3", "V2", 2); + graph.AddEdge("V3", "V4", 2); + graph.AddEdge("V3", "V5", 8); + graph.AddEdge("V3", "V6", 4); + + // V4 + graph.AddEdge("V4", "V6", 6); + + // V5 - No edges + // graph.AddEdge("V5", "?", ?); + + // V6 + graph.AddEdge("V6", "V5", 1); + + return graph; + } + + [Fact] + public void Should_GetVertex() + { + var myGraph = new Graph.Graph(); + myGraph.AddEdge("V0", "V1", 5); + + var myVertex = (Vertex) myGraph.GetVertex("V0"); + + Assert.Equal("V0", myVertex.Name); + } + + [Fact] + public void Should_AddEdge() + { + var myGraph = new Graph.Graph(); + myGraph.AddEdge("V0", "V1", 5); + + var myVertex = (Vertex) myGraph.GetVertex("V0"); + + Assert.Equal(5, myVertex.AdjacentVertices[0].Cost); + } + + [Fact] + public void Should_PathToString() + { + var myGraph = CreateGraph(); + + myGraph.Unweighted("V0"); + + Assert.Equal("(Cost is: 2) V6 to V3 to V0", myGraph.PathToString("V6")); + Assert.Equal("(Cost is: 1) V1 to V0", myGraph.PathToString("V1")); + } + + [Fact] + public void Should_Unweighted() + { + var myGraph = CreateGraph(); + + myGraph.Unweighted("V0"); + + Assert.Equal(0, ((Vertex) myGraph.GetVertex("V0")).Distance); + Assert.Equal(1, ((Vertex) myGraph.GetVertex("V1")).Distance); + Assert.Equal(2, ((Vertex) myGraph.GetVertex("V2")).Distance); + Assert.Equal(1, ((Vertex) myGraph.GetVertex("V3")).Distance); + Assert.Equal(2, ((Vertex) myGraph.GetVertex("V4")).Distance); + Assert.Equal(2, ((Vertex) myGraph.GetVertex("V5")).Distance); + Assert.Equal(2, ((Vertex) myGraph.GetVertex("V6")).Distance); + } + + [Fact] + public void Should_Dijkstra() + { + var myGraph = CreateGraph(); + + myGraph.Dijkstra("V0"); + + Assert.Equal(0, ((Vertex) myGraph.GetVertex("V0")).Distance); + Assert.Equal(2, ((Vertex) myGraph.GetVertex("V1")).Distance); + Assert.Equal(3, ((Vertex) myGraph.GetVertex("V2")).Distance); + Assert.Equal(1, ((Vertex) myGraph.GetVertex("V3")).Distance); + Assert.Equal(3, ((Vertex) myGraph.GetVertex("V4")).Distance); + Assert.Equal(6, ((Vertex) myGraph.GetVertex("V5")).Distance); + Assert.Equal(5, ((Vertex) myGraph.GetVertex("V6")).Distance); + } + + [Fact] + public void Should_ReturnTrue_When_IsConnected() + { + var myGraph = CreateGraph(); + + Assert.True(myGraph.IsConnected()); + } + + [Fact] + public void Should_ReturnFalse_When_IsNotConnected() + { + var myGraph = new Graph.Graph(); + + myGraph.AddEdge("V0", "V1", 2); + myGraph.AddEdge("V2", "V3", 3); + + Assert.False(myGraph.IsConnected()); + } + + [Fact] + public void Should_ToString() + { + var myGraph = CreateGraph(); + + _output.WriteLine(myGraph.ToString()); + } + } +} \ No newline at end of file diff --git a/GraphTests/GraphTests.csproj b/GraphTests/GraphTests.csproj new file mode 100644 index 0000000..9359576 --- /dev/null +++ b/GraphTests/GraphTests.csproj @@ -0,0 +1,20 @@ + + + + netcoreapp2.0 + + false + + + + + + + + + + + + + + diff --git a/PriorityQueueTests/PriorityQueueTests.cs b/PriorityQueueTests/PriorityQueueTests.cs index eedc62a..30c0ab7 100644 --- a/PriorityQueueTests/PriorityQueueTests.cs +++ b/PriorityQueueTests/PriorityQueueTests.cs @@ -8,13 +8,6 @@ namespace PriorityQueueTests { public class PriorityQueueTests { - private readonly ITestOutputHelper _output; - - public PriorityQueueTests(ITestOutputHelper output) - { - _output = output; - } - [Fact] public void Should_IsEmpty() { @@ -65,8 +58,6 @@ public void Should_ToStringPreOrder() var queue = new PriorityQueue(new Collection {92, 47, 21, 20, 12, 25, 63, 61, 17, 55, 37, 45, 64, 83, 73}); - _output.WriteLine(queue.ToStringPreOrder()); - Assert.Equal( "12 17 20 61 92 37 55 47 21 25 45 64 63 83 73 ", queue.ToStringPreOrder() @@ -79,8 +70,6 @@ public void Should_ToStringPostOrder() var queue = new PriorityQueue(new Collection {92, 47, 21, 20, 12, 25, 63, 61, 17, 55, 37, 45, 64, 83, 73}); - _output.WriteLine(queue.ToStringPostOrder()); - Assert.Equal( "61 92 20 55 47 37 17 45 64 25 83 73 63 21 12 ", queue.ToStringPostOrder() @@ -93,8 +82,6 @@ public void Should_ToStringInOrder() var queue = new PriorityQueue(new Collection {92, 47, 21, 20, 12, 25, 63, 61, 17, 55, 37, 45, 64, 83, 73}); - _output.WriteLine(queue.ToStringInOrder()); - Assert.Equal( "61 20 92 17 55 37 47 12 45 25 64 21 83 63 73 ", queue.ToStringInOrder() diff --git a/algoritms-and-datastructures.sln b/algoritms-and-datastructures.sln index 81423ac..e49b6d4 100644 --- a/algoritms-and-datastructures.sln +++ b/algoritms-and-datastructures.sln @@ -75,6 +75,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PriorityQueue", "PriorityQu EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PriorityQueueTests", "PriorityQueueTests\PriorityQueueTests.csproj", "{F048369C-F514-43C4-B3B0-640F2735A9E2}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "graphs", "graphs", "{148BFBB4-E850-4D8A-8FFE-FCB5F02C547F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Graph", "Graph\Graph.csproj", "{8B85E5DC-519C-4499-BB78-3FA23F7FFDBB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GraphTests", "GraphTests\GraphTests.csproj", "{67F98955-F160-451D-89C6-4EE897B4F253}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -205,6 +211,14 @@ Global {F048369C-F514-43C4-B3B0-640F2735A9E2}.Debug|Any CPU.Build.0 = Debug|Any CPU {F048369C-F514-43C4-B3B0-640F2735A9E2}.Release|Any CPU.ActiveCfg = Release|Any CPU {F048369C-F514-43C4-B3B0-640F2735A9E2}.Release|Any CPU.Build.0 = Release|Any CPU + {8B85E5DC-519C-4499-BB78-3FA23F7FFDBB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8B85E5DC-519C-4499-BB78-3FA23F7FFDBB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8B85E5DC-519C-4499-BB78-3FA23F7FFDBB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8B85E5DC-519C-4499-BB78-3FA23F7FFDBB}.Release|Any CPU.Build.0 = Release|Any CPU + {67F98955-F160-451D-89C6-4EE897B4F253}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {67F98955-F160-451D-89C6-4EE897B4F253}.Debug|Any CPU.Build.0 = Debug|Any CPU + {67F98955-F160-451D-89C6-4EE897B4F253}.Release|Any CPU.ActiveCfg = Release|Any CPU + {67F98955-F160-451D-89C6-4EE897B4F253}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -241,5 +255,8 @@ Global {838E2F85-9501-4897-B506-F33BB569098E} = {57920407-C932-4B2E-957E-259C604A71C8} {CDF3B760-6A4E-4252-A268-712ADA9B97D2} = {57920407-C932-4B2E-957E-259C604A71C8} {F048369C-F514-43C4-B3B0-640F2735A9E2} = {57920407-C932-4B2E-957E-259C604A71C8} + {148BFBB4-E850-4D8A-8FFE-FCB5F02C547F} = {678B41A2-BEA6-46E3-A6D6-75ACD6610728} + {8B85E5DC-519C-4499-BB78-3FA23F7FFDBB} = {148BFBB4-E850-4D8A-8FFE-FCB5F02C547F} + {67F98955-F160-451D-89C6-4EE897B4F253} = {148BFBB4-E850-4D8A-8FFE-FCB5F02C547F} EndGlobalSection EndGlobal