diff --git a/Algorithms.Tests/Graph/BipartiteGraphTests.cs b/Algorithms.Tests/Graph/BipartiteGraphTests.cs new file mode 100644 index 00000000..c4459ec5 --- /dev/null +++ b/Algorithms.Tests/Graph/BipartiteGraphTests.cs @@ -0,0 +1,445 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Algorithms.Graph; +using FluentAssertions; +using NUnit.Framework; + +namespace Algorithms.Tests.Graph; + +public class BipartiteGraphTests +{ + [Test] + public void IsBipartite_EmptyGraph_ReturnsTrue() + { + // Arrange + var vertices = Array.Empty(); + IEnumerable GetNeighbors(string v) => Array.Empty(); + + // Act + var result = BipartiteGraph.IsBipartite(vertices, GetNeighbors); + + // Assert + result.Should().BeTrue(); + } + + [Test] + public void IsBipartite_SingleVertex_ReturnsTrue() + { + // Arrange + var vertices = new[] { "A" }; + IEnumerable GetNeighbors(string v) => Array.Empty(); + + // Act + var result = BipartiteGraph.IsBipartite(vertices, GetNeighbors); + + // Assert + result.Should().BeTrue(); + } + + [Test] + public void IsBipartite_TwoVerticesConnected_ReturnsTrue() + { + // Arrange: A - B + var vertices = new[] { "A", "B" }; + IEnumerable GetNeighbors(string v) => v switch + { + "A" => new[] { "B" }, + "B" => new[] { "A" }, + _ => Array.Empty(), + }; + + // Act + var result = BipartiteGraph.IsBipartite(vertices, GetNeighbors); + + // Assert + result.Should().BeTrue(); + } + + [Test] + public void IsBipartite_Triangle_ReturnsFalse() + { + // Arrange: A - B - C - A (odd cycle) + var vertices = new[] { "A", "B", "C" }; + IEnumerable GetNeighbors(string v) => v switch + { + "A" => new[] { "B", "C" }, + "B" => new[] { "A", "C" }, + "C" => new[] { "A", "B" }, + _ => Array.Empty(), + }; + + // Act + var result = BipartiteGraph.IsBipartite(vertices, GetNeighbors); + + // Assert + result.Should().BeFalse(); + } + + [Test] + public void IsBipartite_Square_ReturnsTrue() + { + // Arrange: A - B - C - D - A (even cycle) + var vertices = new[] { "A", "B", "C", "D" }; + IEnumerable GetNeighbors(string v) => v switch + { + "A" => new[] { "B", "D" }, + "B" => new[] { "A", "C" }, + "C" => new[] { "B", "D" }, + "D" => new[] { "A", "C" }, + _ => Array.Empty(), + }; + + // Act + var result = BipartiteGraph.IsBipartite(vertices, GetNeighbors); + + // Assert + result.Should().BeTrue(); + } + + [Test] + public void IsBipartite_CompleteBipartiteK23_ReturnsTrue() + { + // Arrange: Complete bipartite K(2,3) + var vertices = new[] { "A", "B", "X", "Y", "Z" }; + IEnumerable GetNeighbors(string v) => v switch + { + "A" => new[] { "X", "Y", "Z" }, + "B" => new[] { "X", "Y", "Z" }, + "X" => new[] { "A", "B" }, + "Y" => new[] { "A", "B" }, + "Z" => new[] { "A", "B" }, + _ => Array.Empty(), + }; + + // Act + var result = BipartiteGraph.IsBipartite(vertices, GetNeighbors); + + // Assert + result.Should().BeTrue(); + } + + [Test] + public void IsBipartite_DisconnectedBipartiteComponents_ReturnsTrue() + { + // Arrange: (A-B) and (C-D) + var vertices = new[] { "A", "B", "C", "D" }; + IEnumerable GetNeighbors(string v) => v switch + { + "A" => new[] { "B" }, + "B" => new[] { "A" }, + "C" => new[] { "D" }, + "D" => new[] { "C" }, + _ => Array.Empty(), + }; + + // Act + var result = BipartiteGraph.IsBipartite(vertices, GetNeighbors); + + // Assert + result.Should().BeTrue(); + } + + [Test] + public void IsBipartite_DisconnectedWithOddCycle_ReturnsFalse() + { + // Arrange: (A-B) and (C-D-E-C triangle) + var vertices = new[] { "A", "B", "C", "D", "E" }; + IEnumerable GetNeighbors(string v) => v switch + { + "A" => new[] { "B" }, + "B" => new[] { "A" }, + "C" => new[] { "D", "E" }, + "D" => new[] { "C", "E" }, + "E" => new[] { "C", "D" }, + _ => Array.Empty(), + }; + + // Act + var result = BipartiteGraph.IsBipartite(vertices, GetNeighbors); + + // Assert + result.Should().BeFalse(); + } + + [Test] + public void IsBipartite_StarGraph_ReturnsTrue() + { + // Arrange: Star with center A + var vertices = new[] { "A", "B", "C", "D" }; + IEnumerable GetNeighbors(string v) => v switch + { + "A" => new[] { "B", "C", "D" }, + "B" => new[] { "A" }, + "C" => new[] { "A" }, + "D" => new[] { "A" }, + _ => Array.Empty(), + }; + + // Act + var result = BipartiteGraph.IsBipartite(vertices, GetNeighbors); + + // Assert + result.Should().BeTrue(); + } + + [Test] + public void IsBipartite_Pentagon_ReturnsFalse() + { + // Arrange: Pentagon (5-cycle) + var vertices = new[] { 1, 2, 3, 4, 5 }; + IEnumerable GetNeighbors(int v) => v switch + { + 1 => new[] { 2, 5 }, + 2 => new[] { 1, 3 }, + 3 => new[] { 2, 4 }, + 4 => new[] { 3, 5 }, + 5 => new[] { 1, 4 }, + _ => Array.Empty(), + }; + + // Act + var result = BipartiteGraph.IsBipartite(vertices, GetNeighbors); + + // Assert + result.Should().BeFalse(); + } + + [Test] + public void IsBipartite_NullVertices_ThrowsArgumentNullException() + { + // Act + Action act = () => BipartiteGraph.IsBipartite(null!, v => Array.Empty()); + + // Assert + act.Should().Throw().WithParameterName("vertices"); + } + + [Test] + public void IsBipartite_NullGetNeighbors_ThrowsArgumentNullException() + { + // Arrange + var vertices = new[] { "A" }; + + // Act + Action act = () => BipartiteGraph.IsBipartite(vertices, null!); + + // Assert + act.Should().Throw().WithParameterName("getNeighbors"); + } + + [Test] + public void GetPartitions_BipartiteGraph_ReturnsCorrectSets() + { + // Arrange: A - B - C - D (chain) + var vertices = new[] { "A", "B", "C", "D" }; + IEnumerable GetNeighbors(string v) => v switch + { + "A" => new[] { "B" }, + "B" => new[] { "A", "C" }, + "C" => new[] { "B", "D" }, + "D" => new[] { "C" }, + _ => Array.Empty(), + }; + + // Act + var result = BipartiteGraph.GetPartitions(vertices, GetNeighbors); + + // Assert + result.Should().NotBeNull(); + result!.Value.SetA.Should().Contain(new[] { "A", "C" }); + result.Value.SetB.Should().Contain(new[] { "B", "D" }); + } + + [Test] + public void GetPartitions_NonBipartiteGraph_ReturnsNull() + { + // Arrange: Triangle + var vertices = new[] { "A", "B", "C" }; + IEnumerable GetNeighbors(string v) => v switch + { + "A" => new[] { "B", "C" }, + "B" => new[] { "A", "C" }, + "C" => new[] { "A", "B" }, + _ => Array.Empty(), + }; + + // Act + var result = BipartiteGraph.GetPartitions(vertices, GetNeighbors); + + // Assert + result.Should().BeNull(); + } + + [Test] + public void GetPartitions_EmptyGraph_ReturnsEmptySets() + { + // Arrange + var vertices = Array.Empty(); + IEnumerable GetNeighbors(string v) => Array.Empty(); + + // Act + var result = BipartiteGraph.GetPartitions(vertices, GetNeighbors); + + // Assert + result.Should().NotBeNull(); + result!.Value.SetA.Should().BeEmpty(); + result.Value.SetB.Should().BeEmpty(); + } + + [Test] + public void GetPartitions_NullVertices_ThrowsArgumentNullException() + { + // Act + Action act = () => BipartiteGraph.GetPartitions(null!, v => Array.Empty()); + + // Assert + act.Should().Throw().WithParameterName("vertices"); + } + + [Test] + public void GetPartitions_NullGetNeighbors_ThrowsArgumentNullException() + { + // Arrange + var vertices = new[] { "A" }; + + // Act + Action act = () => BipartiteGraph.GetPartitions(vertices, null!); + + // Assert + act.Should().Throw().WithParameterName("getNeighbors"); + } + + [Test] + public void IsBipartiteDfs_BipartiteGraph_ReturnsTrue() + { + // Arrange: Square + var vertices = new[] { "A", "B", "C", "D" }; + IEnumerable GetNeighbors(string v) => v switch + { + "A" => new[] { "B", "D" }, + "B" => new[] { "A", "C" }, + "C" => new[] { "B", "D" }, + "D" => new[] { "A", "C" }, + _ => Array.Empty(), + }; + + // Act + var result = BipartiteGraph.IsBipartiteDfs(vertices, GetNeighbors); + + // Assert + result.Should().BeTrue(); + } + + [Test] + public void IsBipartiteDfs_NonBipartiteGraph_ReturnsFalse() + { + // Arrange: Triangle + var vertices = new[] { "A", "B", "C" }; + IEnumerable GetNeighbors(string v) => v switch + { + "A" => new[] { "B", "C" }, + "B" => new[] { "A", "C" }, + "C" => new[] { "A", "B" }, + _ => Array.Empty(), + }; + + // Act + var result = BipartiteGraph.IsBipartiteDfs(vertices, GetNeighbors); + + // Assert + result.Should().BeFalse(); + } + + [Test] + public void IsBipartiteDfs_NullVertices_ThrowsArgumentNullException() + { + // Act + Action act = () => BipartiteGraph.IsBipartiteDfs(null!, v => Array.Empty()); + + // Assert + act.Should().Throw().WithParameterName("vertices"); + } + + [Test] + public void IsBipartiteDfs_NullGetNeighbors_ThrowsArgumentNullException() + { + // Arrange + var vertices = new[] { "A" }; + + // Act + Action act = () => BipartiteGraph.IsBipartiteDfs(vertices, null!); + + // Assert + act.Should().Throw().WithParameterName("getNeighbors"); + } + + [Test] + public void IsBipartite_LargeEvenCycle_ReturnsTrue() + { + // Arrange: Large even cycle (100 vertices) + var vertices = Enumerable.Range(0, 100).ToArray(); + IEnumerable GetNeighbors(int v) + { + var neighbors = new List + { + v > 0 ? v - 1 : 99, // Previous or close cycle + v < 99 ? v + 1 : 0, // Next or close cycle + }; + + return neighbors; + } + + // Act + var result = BipartiteGraph.IsBipartite(vertices, GetNeighbors); + + // Assert + result.Should().BeTrue(); + } + + [Test] + public void IsBipartite_LargeOddCycle_ReturnsFalse() + { + // Arrange: Large odd cycle (101 vertices) + var vertices = Enumerable.Range(0, 101).ToArray(); + IEnumerable GetNeighbors(int v) + { + var neighbors = new List + { + v > 0 ? v - 1 : 100, // Previous or close cycle + v < 100 ? v + 1 : 0, // Next or close cycle + }; + + return neighbors; + } + + // Act + var result = BipartiteGraph.IsBipartite(vertices, GetNeighbors); + + // Assert + result.Should().BeFalse(); + } + + [Test] + public void GetPartitions_CompleteBipartite_ReturnsCorrectSets() + { + // Arrange: K(2,2) + var vertices = new[] { "A", "B", "X", "Y" }; + IEnumerable GetNeighbors(string v) => v switch + { + "A" => new[] { "X", "Y" }, + "B" => new[] { "X", "Y" }, + "X" => new[] { "A", "B" }, + "Y" => new[] { "A", "B" }, + _ => Array.Empty(), + }; + + // Act + var result = BipartiteGraph.GetPartitions(vertices, GetNeighbors); + + // Assert + result.Should().NotBeNull(); + result!.Value.SetA.Should().HaveCount(2); + result.Value.SetB.Should().HaveCount(2); + } +} diff --git a/Algorithms/Graph/BipartiteGraph.cs b/Algorithms/Graph/BipartiteGraph.cs new file mode 100644 index 00000000..1b00323a --- /dev/null +++ b/Algorithms/Graph/BipartiteGraph.cs @@ -0,0 +1,233 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Algorithms.Graph; + +/// +/// Checks if a graph is bipartite (2-colorable). +/// A bipartite graph can be divided into two independent sets where no two vertices +/// within the same set are adjacent. +/// +public static class BipartiteGraph +{ + /// + /// Checks if a graph is bipartite using BFS-based coloring. + /// + /// Type of vertex. + /// All vertices in the graph. + /// Function to get neighbors of a vertex. + /// True if graph is bipartite, false otherwise. + public static bool IsBipartite( + IEnumerable vertices, + Func> getNeighbors) where T : notnull + { + if (vertices == null) + { + throw new ArgumentNullException(nameof(vertices)); + } + + if (getNeighbors == null) + { + throw new ArgumentNullException(nameof(getNeighbors)); + } + + var vertexList = vertices.ToList(); + if (vertexList.Count == 0) + { + return true; // Empty graph is bipartite + } + + var colors = new Dictionary(); + + // Check each connected component + foreach (var start in vertexList) + { + if (colors.ContainsKey(start)) + { + continue; // Already colored + } + + if (!BfsColor(start, colors, getNeighbors)) + { + return false; + } + } + + return true; + } + + /// + /// Gets the two partitions of a bipartite graph. + /// + /// Type of vertex. + /// All vertices in the graph. + /// Function to get neighbors of a vertex. + /// Tuple of two sets representing the partitions, or null if not bipartite. + public static (HashSet SetA, HashSet SetB)? GetPartitions( + IEnumerable vertices, + Func> getNeighbors) where T : notnull + { + if (vertices == null) + { + throw new ArgumentNullException(nameof(vertices)); + } + + if (getNeighbors == null) + { + throw new ArgumentNullException(nameof(getNeighbors)); + } + + var vertexList = vertices.ToList(); + if (vertexList.Count == 0) + { + return (new HashSet(), new HashSet()); + } + + var colors = new Dictionary(); + + // Color all components + foreach (var start in vertexList) + { + if (colors.ContainsKey(start)) + { + continue; + } + + if (!BfsColor(start, colors, getNeighbors)) + { + return null; // Not bipartite + } + } + + // Split into two sets based on color + var setA = new HashSet(); + var setB = new HashSet(); + + foreach (var vertex in vertexList) + { + if (colors[vertex] == 0) + { + setA.Add(vertex); + } + else + { + setB.Add(vertex); + } + } + + return (setA, setB); + } + + /// + /// Checks if a graph is bipartite using DFS-based coloring. + /// + /// Type of vertex. + /// All vertices in the graph. + /// Function to get neighbors of a vertex. + /// True if graph is bipartite, false otherwise. + public static bool IsBipartiteDfs( + IEnumerable vertices, + Func> getNeighbors) where T : notnull + { + if (vertices == null) + { + throw new ArgumentNullException(nameof(vertices)); + } + + if (getNeighbors == null) + { + throw new ArgumentNullException(nameof(getNeighbors)); + } + + var vertexList = vertices.ToList(); + if (vertexList.Count == 0) + { + return true; + } + + var colors = new Dictionary(); + + foreach (var start in vertexList) + { + if (colors.ContainsKey(start)) + { + continue; + } + + if (!DfsColor(start, 0, colors, getNeighbors)) + { + return false; + } + } + + return true; + } + + private static bool BfsColor( + T start, + Dictionary colors, + Func> getNeighbors) where T : notnull + { + var queue = new Queue(); + queue.Enqueue(start); + colors[start] = 0; + + while (queue.Count > 0) + { + var current = queue.Dequeue(); + var currentColor = colors[current]; + var nextColor = 1 - currentColor; + + foreach (var neighbor in getNeighbors(current)) + { + if (!colors.ContainsKey(neighbor)) + { + colors[neighbor] = nextColor; + queue.Enqueue(neighbor); + } + else if (colors[neighbor] == currentColor) + { + return false; // Same color as current - not bipartite + } + else + { + // Different color - valid + } + } + } + + return true; + } + + private static bool DfsColor( + T vertex, + int color, + Dictionary colors, + Func> getNeighbors) where T : notnull + { + colors[vertex] = color; + var nextColor = 1 - color; + + foreach (var neighbor in getNeighbors(vertex)) + { + if (!colors.ContainsKey(neighbor)) + { + if (!DfsColor(neighbor, nextColor, colors, getNeighbors)) + { + return false; + } + } + else if (colors[neighbor] == color) + { + return false; // Same color - not bipartite + } + else + { + // Different color - valid + } + } + + return true; + } +}