diff --git a/Graph/DirectedGraph.cs b/Graph/DirectedGraph.cs index 988183d..19e241b 100644 --- a/Graph/DirectedGraph.cs +++ b/Graph/DirectedGraph.cs @@ -33,6 +33,9 @@ namespace MoonTools.Core.Graph public IEnumerable Nodes => nodes; public IEnumerable<(TNode, TNode)> Edges => edges; + public int Order => nodes.Count; + public int Size => edges.Count; + public void AddNode(TNode node) { if (!Exists(node)) @@ -71,6 +74,12 @@ namespace MoonTools.Core.Graph } } + public int Degree(TNode node) + { + CheckNodes(node); + return neighbors[node].Count; + } + readonly List<(TNode, TNode)> edgesToRemove = new List<(TNode, TNode)>(); public void RemoveNode(TNode node) @@ -96,7 +105,7 @@ namespace MoonTools.Core.Graph neighbors.Remove(node); } - public void AddEdge(TNode v, TNode u, TEdgeData edgeData) + public virtual void AddEdge(TNode v, TNode u, TEdgeData edgeData) { CheckNodes(v, u); @@ -105,7 +114,7 @@ namespace MoonTools.Core.Graph edgesToEdgeData.Add((v, u), edgeData); } - public void AddEdges(params (TNode, TNode, TEdgeData)[] edges) + public virtual void AddEdges(params (TNode, TNode, TEdgeData)[] edges) { foreach (var edge in edges) { @@ -119,10 +128,11 @@ namespace MoonTools.Core.Graph if (!Exists(v, u)) { throw new ArgumentException($"Edge between vertex {v} and vertex {u} does not exist in the graph"); } } - public void RemoveEdge(TNode v, TNode u) + public virtual void RemoveEdge(TNode v, TNode u) { CheckEdge(v, u); neighbors[v].Remove(u); + edges.Remove((v, u)); edgesToEdgeData.Remove((v, u)); } @@ -139,40 +149,167 @@ namespace MoonTools.Core.Graph return neighbors[node]; } - readonly HashSet discovered = new HashSet(); - readonly Dictionary dfsOutput = new Dictionary(); + readonly Stack dfsStack = new Stack(); + readonly HashSet dfsDiscovered = new HashSet(); - public IEnumerable<(TNode, uint)> NodeDFS() + public IEnumerable PreorderNodeDFS() { - discovered.Clear(); - dfsOutput.Clear(); - uint time = 0; + dfsStack.Clear(); + dfsDiscovered.Clear(); + + foreach (var node in Nodes) + { + if (!dfsDiscovered.Contains(node)) + { + dfsStack.Push(node); + while (dfsStack.Count > 0) + { + var current = dfsStack.Pop(); + if (!dfsDiscovered.Contains(current)) + { + dfsDiscovered.Add(current); + yield return current; + foreach (var neighbor in Neighbors(current)) + { + dfsStack.Push(neighbor); + } + } + } + } + } + } + + // public IEnumerable PostorderNodeDFS() + // { + // dfsStack.Clear(); + // dfsDiscovered.Clear(); + + // foreach (var node in Nodes) + // { + // if (!dfsDiscovered.Contains(node)) + // { + // dfsStack.Push(node); + // while (dfsStack.Count > 0) + // { + // var current = dfsStack.Pop(); + // if (!dfsDiscovered.Contains(current)) + // { + // dfsDiscovered.Add(current); + // foreach (var neighbor in Neighbors(current)) + // { + // dfsStack.Push(neighbor); + // } + // yield return current; + // } + // } + // } + // } + // } + + List postorderOutput = new List(); + + public IEnumerable PostorderNodeDFS() + { + dfsDiscovered.Clear(); + postorderOutput.Clear(); void dfsHelper(TNode v) // refactor this to remove closure { - discovered.Add(v); + dfsDiscovered.Add(v); foreach (var neighbor in Neighbors(v)) { - if (!discovered.Contains(neighbor)) + if (!dfsDiscovered.Contains(neighbor)) { dfsHelper(neighbor); } } - time++; - dfsOutput[v] = time; + postorderOutput.Add(v); } foreach (var node in Nodes) { - if (!discovered.Contains(node)) + if (!dfsDiscovered.Contains(node)) { dfsHelper(node); } } - return dfsOutput.Select(entry => (entry.Key, entry.Value)); + return postorderOutput; + } + + readonly Queue bfsQueue = new Queue(); + readonly HashSet bfsDiscovered = new HashSet(); + + public IEnumerable NodeBFS() + { + bfsQueue.Clear(); + bfsDiscovered.Clear(); + + foreach (var node in Nodes) + { + if (!bfsDiscovered.Contains(node)) + { + bfsQueue.Enqueue(node); + while (bfsQueue.Count > 0) + { + var current = bfsQueue.Dequeue(); + foreach (var neighbor in Neighbors(current)) + { + if (!bfsDiscovered.Contains(neighbor)) + { + bfsDiscovered.Add(neighbor); + bfsQueue.Enqueue(neighbor); + yield return neighbor; + } + } + } + } + } + } + + // hoo boy this is bad for the GC + public IEnumerable LexicographicBFS() + { + var sets = new List>(); + sets.Add(Nodes.ToList()); + + while (sets.Count > 0) + { + var firstSet = sets[0]; + var node = firstSet[0]; + firstSet.RemoveAt(0); + if (firstSet.Count == 0) { sets.RemoveAt(0); } + + yield return node; + + var replaced = new List>(); + + foreach (var neighbor in Neighbors(node)) + { + if (sets.Any(set => set.Contains(neighbor))) + { + var s = sets.Find(set => set.Contains(neighbor)); + var sIndex = sets.IndexOf(s); + List t; + if (replaced.Contains(s)) + { + t = sets[sIndex - 1]; + } + else + { + t = new List(); + sets.Insert(sIndex, t); + replaced.Add(s); + } + + s.Remove(neighbor); + t.Add(neighbor); + if (s.Count == 0) { sets.Remove(s); } + } + } + } } public bool Cyclic() @@ -182,7 +319,7 @@ namespace MoonTools.Core.Graph public IEnumerable TopologicalSort() { - return NodeDFS().OrderByDescending(entry => entry.Item2).Select(entry => entry.Item1); + return PostorderNodeDFS().Reverse(); } readonly Dictionary preorder = new Dictionary(); diff --git a/Graph/UndirectedGraph.cs b/Graph/UndirectedGraph.cs new file mode 100644 index 0000000..ddc5be4 --- /dev/null +++ b/Graph/UndirectedGraph.cs @@ -0,0 +1,100 @@ +using System.Linq; +using System.Collections.Generic; + +namespace MoonTools.Core.Graph +{ + public class UndirectedGraph : DirectedGraph where TNode : System.IEquatable + { + enum Color { White, Gray, Black } + + new public int Size => edges.Count / 2; + + public bool Complete => Size == (Order * (Order - 1) / 2); + + public bool Chordal + { + get + { + var lexicographicOrder = LexicographicBFS(); + return lexicographicOrder + .Select((node, index) => (node, index)) + .All(pair => + { + var (node, index) = pair; + var successors = lexicographicOrder.Skip(index - 1).Take(nodes.Count - index - 1); + return Clique(Neighbors(node).Intersect(successors).Union(Enumerable.Repeat(node, 1))); + }); + } + } + + public bool Bipartite + { + get + { + var colors = new Dictionary(); + var d = new Dictionary(); + var partition = new Dictionary(); + + foreach (var node in Nodes) + { + colors[node] = Color.White; + d[node] = int.MaxValue; + partition[node] = 0; + } + + var start = Nodes.First(); + colors[start] = Color.Gray; + partition[start] = 1; + d[start] = 0; + + var stack = new Stack(); + stack.Push(start); + + while (stack.Count > 0) + { + var node = stack.Pop(); + foreach (var neighbor in Neighbors(node)) + { + if (partition[neighbor] == partition[node]) { return false; } + if (colors[neighbor] == Color.White) + { + colors[neighbor] = Color.Gray; + d[neighbor] = d[node] + 1; + partition[neighbor] = 3 - partition[node]; + stack.Push(neighbor); + } + } + stack.Pop(); + colors[node] = Color.Black; + } + + return true; + } + } + + public override void AddEdge(TNode v, TNode u, TEdgeData edgeData) + { + base.AddEdge(v, u, edgeData); + base.AddEdge(u, v, edgeData); + } + + public override void AddEdges(params (TNode, TNode, TEdgeData)[] edges) + { + foreach (var edge in edges) + { + AddEdge(edge.Item1, edge.Item2, edge.Item3); + } + } + + public override void RemoveEdge(TNode v, TNode u) + { + base.RemoveEdge(v, u); + base.RemoveEdge(u, v); + } + + public bool Clique(IEnumerable nodeList) + { + return nodeList.All(node => nodeList.All(other => Neighbors(node).Contains(other) || node.Equals(other))); + } + } +} \ No newline at end of file diff --git a/test/DirectedGraphTest.cs b/test/DirectedGraph.cs similarity index 81% rename from test/DirectedGraphTest.cs rename to test/DirectedGraph.cs index b343227..84daa47 100644 --- a/test/DirectedGraphTest.cs +++ b/test/DirectedGraph.cs @@ -61,6 +61,47 @@ namespace Tests Assert.That(myGraph.Neighbors(1), Does.Not.Contain(4)); } + [Test] + public void Order() + { + var myGraph = new DirectedGraph(); + myGraph.AddNodes(1, 2, 3, 4); + + myGraph.Order.Should().Be(4); + } + + [Test] + public void Size() + { + var myGraph = new DirectedGraph(); + myGraph.AddNodes(1, 2, 3, 4); + myGraph.AddEdges( + (1, 2, dummyEdgeData), + (2, 3, dummyEdgeData), + (2, 4, dummyEdgeData), + (3, 4, dummyEdgeData) + ); + + myGraph.Size.Should().Be(4); + } + + [Test] + public void Degree() + { + var myGraph = new DirectedGraph(); + myGraph.AddNodes(1, 2, 3, 4); + myGraph.AddEdges( + (1, 2, dummyEdgeData), + (2, 3, dummyEdgeData), + (2, 4, dummyEdgeData), + (3, 4, dummyEdgeData) + ); + + myGraph.Degree(1).Should().Be(1); + myGraph.Degree(2).Should().Be(2); + myGraph.Degree(3).Should().Be(1); + } + [Test] public void RemoveEdge() { @@ -106,15 +147,57 @@ namespace Tests myGraph.AddEdges( ('a', 'b', dummyEdgeData), ('a', 'c', dummyEdgeData), - ('b', 'd', dummyEdgeData) + ('b', 'd', dummyEdgeData), + ('c', 'd', dummyEdgeData) ); - var result = myGraph.NodeDFS(); + var result = myGraph.PreorderNodeDFS().ToList(); - result.Should().Contain(('d', 1)); - result.Should().Contain(('b', 2)); - result.Should().Contain(('c', 3)); - result.Should().Contain(('a', 4)); + var indexA = result.IndexOf('a'); + var indexB = result.IndexOf('b'); + var indexC = result.IndexOf('c'); + var indexD = result.IndexOf('d'); + + Assert.That(indexA < indexB && indexA < indexC); + Assert.That(indexB < indexD || indexC < indexD); + } + + [Test] + public void NodeBFS() + { + var myGraph = new DirectedGraph(); + myGraph.AddNodes('a', 'b', 'c', 'd', 'e'); + myGraph.AddEdges( + ('a', 'b', dummyEdgeData), + ('a', 'c', dummyEdgeData), + ('b', 'd', dummyEdgeData), + ('c', 'e', dummyEdgeData) + ); + + var result = myGraph.NodeBFS().ToList(); + + result.IndexOf('a').Should().BeLessThan(result.IndexOf('b')); + result.IndexOf('a').Should().BeLessThan(result.IndexOf('c')); + result.IndexOf('b').Should().BeLessThan(result.IndexOf('d')); + result.IndexOf('c').Should().BeLessThan(result.IndexOf('e')); + } + + [Test] + public void LexicographicBFS() + { + var graph = new DirectedGraph(); + graph.AddNodes('a', 'b', 'c', 'd', 'e'); + graph.AddEdges( + ('a', 'b', dummyEdgeData), + ('a', 'c', dummyEdgeData), + ('b', 'c', dummyEdgeData), + ('b', 'd', dummyEdgeData), + ('b', 'e', dummyEdgeData), + ('c', 'd', dummyEdgeData), + ('d', 'e', dummyEdgeData) + ); + + graph.LexicographicBFS().Should().ContainInOrder(new char[] { 'a', 'b', 'c', 'd', 'e' }); } [Test] diff --git a/test/UndirectedGraph.cs b/test/UndirectedGraph.cs new file mode 100644 index 0000000..8b63ad6 --- /dev/null +++ b/test/UndirectedGraph.cs @@ -0,0 +1,134 @@ +using NUnit.Framework; +using FluentAssertions; + +using MoonTools.Core.Graph; + +namespace Tests +{ + public class UndirectedGraphTests + { + EdgeData dummyEdgeData; + + [Test] + public void Size() + { + var graph = new UndirectedGraph(); + graph.AddNodes('a', 'b', 'c', 'd', 'e'); + graph.AddEdges( + ('a', 'b', dummyEdgeData), + ('a', 'c', dummyEdgeData), + ('a', 'd', dummyEdgeData), + ('b', 'c', dummyEdgeData), + ('b', 'd', dummyEdgeData), + ('c', 'd', dummyEdgeData), + ('c', 'e', dummyEdgeData) + ); + + graph.Size.Should().Be(7); + } + + [Test] + public void Degree() + { + var graph = new UndirectedGraph(); + graph.AddNodes('a', 'b', 'c', 'd', 'e'); + graph.AddEdges( + ('a', 'b', dummyEdgeData), + ('a', 'c', dummyEdgeData), + ('a', 'd', dummyEdgeData), + ('b', 'c', dummyEdgeData), + ('b', 'd', dummyEdgeData), + ('c', 'd', dummyEdgeData), + ('c', 'e', dummyEdgeData) + ); + + graph.Degree('a').Should().Be(3); + graph.Degree('b').Should().Be(3); + graph.Degree('c').Should().Be(4); + graph.Degree('d').Should().Be(3); + graph.Degree('e').Should().Be(1); + } + + [Test] + public void Clique() + { + var graph = new UndirectedGraph(); + graph.AddNodes('a', 'b', 'c', 'd', 'e'); + graph.AddEdges( + ('a', 'b', dummyEdgeData), + ('a', 'c', dummyEdgeData), + ('a', 'd', dummyEdgeData), + ('b', 'c', dummyEdgeData), + ('b', 'd', dummyEdgeData), + ('c', 'd', dummyEdgeData), + ('c', 'e', dummyEdgeData) + ); + + graph.Clique(new char[] { 'a', 'b', 'c', 'd' }).Should().BeTrue(); + graph.Clique(new char[] { 'a', 'b', 'c', 'd', 'e' }).Should().BeFalse(); + } + + [Test] + public void Chordal() + { + var graph = new UndirectedGraph(); + graph.AddNodes('a', 'b', 'c', 'd', 'e'); + graph.AddEdges( + ('a', 'b', dummyEdgeData), + ('a', 'c', dummyEdgeData), + ('b', 'c', dummyEdgeData), + ('b', 'd', dummyEdgeData), + ('b', 'e', dummyEdgeData), + ('c', 'd', dummyEdgeData), + ('d', 'e', dummyEdgeData) + ); + + graph.Chordal.Should().BeTrue(); + + graph.AddNode('f'); + graph.AddEdge('a', 'f', dummyEdgeData); + + graph.Chordal.Should().BeFalse(); + } + + [Test] + public void Complete() + { + var graph = new UndirectedGraph(); + graph.AddNodes('a', 'b', 'c', 'd'); + graph.AddEdges( + ('a', 'b', dummyEdgeData), + ('a', 'c', dummyEdgeData), + ('a', 'd', dummyEdgeData), + ('b', 'c', dummyEdgeData), + ('b', 'd', dummyEdgeData), + ('c', 'd', dummyEdgeData) + ); + + graph.Complete.Should().BeTrue(); + + graph.RemoveEdge('b', 'c'); + + graph.Complete.Should().BeFalse(); + } + + [Test] + public void Bipartite() + { + var graph = new UndirectedGraph(); + graph.AddNodes('a', 'b', 'c', 'd', 'e'); + graph.AddEdges( + ('a', 'b', dummyEdgeData), + ('a', 'c', dummyEdgeData), + ('b', 'd', dummyEdgeData), + ('c', 'e', dummyEdgeData) + ); + + graph.Bipartite.Should().BeTrue(); + + graph.AddEdge('a', 'e', dummyEdgeData); + + graph.Bipartite.Should().BeFalse(); + } + } +} \ No newline at end of file