diff --git a/Algorithms.Tests/Graph/ArticulationPointsTests.cs b/Algorithms.Tests/Graph/ArticulationPointsTests.cs new file mode 100644 index 00000000..3871a6e8 --- /dev/null +++ b/Algorithms.Tests/Graph/ArticulationPointsTests.cs @@ -0,0 +1,326 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Algorithms.Graph; +using FluentAssertions; +using NUnit.Framework; + +namespace Algorithms.Tests.Graph; + +public class ArticulationPointsTests +{ + [Test] + public void Find_SimpleChain_ReturnsMiddleVertex() + { + // Arrange: A - B - C (B is articulation point) + var vertices = new[] { "A", "B", "C" }; + IEnumerable GetNeighbors(string v) => v switch + { + "A" => new[] { "B" }, + "B" => new[] { "A", "C" }, + "C" => new[] { "B" }, + _ => Array.Empty(), + }; + + // Act + var result = ArticulationPoints.Find(vertices, GetNeighbors); + + // Assert + result.Should().ContainSingle(); + result.Should().Contain("B"); + } + + [Test] + public void Find_Triangle_ReturnsEmpty() + { + // Arrange: A - B - C - A (no articulation points) + 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 = ArticulationPoints.Find(vertices, GetNeighbors); + + // Assert + result.Should().BeEmpty(); + } + + [Test] + public void Find_StarGraph_ReturnsCenterVertex() + { + // 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 = ArticulationPoints.Find(vertices, GetNeighbors); + + // Assert + result.Should().ContainSingle(); + result.Should().Contain("A"); + } + + [Test] + public void Find_BridgeGraph_ReturnsMultiplePoints() + { + // Arrange: (A-B-C) - D - (E-F-G) + var vertices = new[] { "A", "B", "C", "D", "E", "F", "G" }; + IEnumerable GetNeighbors(string v) => v switch + { + "A" => new[] { "B" }, + "B" => new[] { "A", "C", "D" }, + "C" => new[] { "B" }, + "D" => new[] { "B", "E" }, + "E" => new[] { "D", "F" }, + "F" => new[] { "E", "G" }, + "G" => new[] { "F" }, + _ => Array.Empty(), + }; + + // Act + var result = ArticulationPoints.Find(vertices, GetNeighbors); + + // Assert + result.Should().HaveCount(4); + result.Should().Contain(new[] { "B", "D", "E", "F" }); + } + + [Test] + public void Find_DisconnectedGraph_FindsPointsInEachComponent() + { + // Arrange: (A-B-C) and (D-E-F) + var vertices = new[] { "A", "B", "C", "D", "E", "F" }; + IEnumerable GetNeighbors(string v) => v switch + { + "A" => new[] { "B" }, + "B" => new[] { "A", "C" }, + "C" => new[] { "B" }, + "D" => new[] { "E" }, + "E" => new[] { "D", "F" }, + "F" => new[] { "E" }, + _ => Array.Empty(), + }; + + // Act + var result = ArticulationPoints.Find(vertices, GetNeighbors); + + // Assert + result.Should().HaveCount(2); + result.Should().Contain(new[] { "B", "E" }); + } + + [Test] + public void Find_SingleVertex_ReturnsEmpty() + { + // Arrange + var vertices = new[] { "A" }; + IEnumerable GetNeighbors(string v) => Array.Empty(); + + // Act + var result = ArticulationPoints.Find(vertices, GetNeighbors); + + // Assert + result.Should().BeEmpty(); + } + + [Test] + public void Find_TwoVertices_ReturnsEmpty() + { + // 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 = ArticulationPoints.Find(vertices, GetNeighbors); + + // Assert + result.Should().BeEmpty(); + } + + [Test] + public void Find_ComplexGraph_ReturnsCorrectPoints() + { + // Arrange: Complex graph with multiple articulation points + var vertices = new[] { 1, 2, 3, 4, 5, 6, 7 }; + IEnumerable GetNeighbors(int v) => v switch + { + 1 => new[] { 2, 3 }, + 2 => new[] { 1, 3 }, + 3 => new[] { 1, 2, 4 }, + 4 => new[] { 3, 5, 6 }, + 5 => new[] { 4, 6 }, + 6 => new[] { 4, 5, 7 }, + 7 => new[] { 6 }, + _ => Array.Empty(), + }; + + // Act + var result = ArticulationPoints.Find(vertices, GetNeighbors); + + // Assert + result.Should().Contain(new[] { 3, 4, 6 }); + } + + [Test] + public void Find_EmptyGraph_ReturnsEmpty() + { + // Arrange + var vertices = Array.Empty(); + IEnumerable GetNeighbors(string v) => Array.Empty(); + + // Act + var result = ArticulationPoints.Find(vertices, GetNeighbors); + + // Assert + result.Should().BeEmpty(); + } + + [Test] + public void Find_NullVertices_ThrowsArgumentNullException() + { + // Act + Action act = () => ArticulationPoints.Find(null!, v => Array.Empty()); + + // Assert + act.Should().Throw().WithParameterName("vertices"); + } + + [Test] + public void Find_NullGetNeighbors_ThrowsArgumentNullException() + { + // Arrange + var vertices = new[] { "A" }; + + // Act + Action act = () => ArticulationPoints.Find(vertices, null!); + + // Assert + act.Should().Throw().WithParameterName("getNeighbors"); + } + + [Test] + public void IsArticulationPoint_ValidPoint_ReturnsTrue() + { + // Arrange: A - B - C + var vertices = new[] { "A", "B", "C" }; + IEnumerable GetNeighbors(string v) => v switch + { + "A" => new[] { "B" }, + "B" => new[] { "A", "C" }, + "C" => new[] { "B" }, + _ => Array.Empty(), + }; + + // Act + var result = ArticulationPoints.IsArticulationPoint("B", vertices, GetNeighbors); + + // Assert + result.Should().BeTrue(); + } + + [Test] + public void IsArticulationPoint_NotArticulationPoint_ReturnsFalse() + { + // Arrange: A - B - C + var vertices = new[] { "A", "B", "C" }; + IEnumerable GetNeighbors(string v) => v switch + { + "A" => new[] { "B" }, + "B" => new[] { "A", "C" }, + "C" => new[] { "B" }, + _ => Array.Empty(), + }; + + // Act + var result = ArticulationPoints.IsArticulationPoint("A", vertices, GetNeighbors); + + // Assert + result.Should().BeFalse(); + } + + [Test] + public void Count_SimpleChain_ReturnsOne() + { + // Arrange: A - B - C + var vertices = new[] { "A", "B", "C" }; + IEnumerable GetNeighbors(string v) => v switch + { + "A" => new[] { "B" }, + "B" => new[] { "A", "C" }, + "C" => new[] { "B" }, + _ => Array.Empty(), + }; + + // Act + var result = ArticulationPoints.Count(vertices, GetNeighbors); + + // Assert + result.Should().Be(1); + } + + [Test] + public void Count_Triangle_ReturnsZero() + { + // Arrange: A - B - C - A + 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 = ArticulationPoints.Count(vertices, GetNeighbors); + + // Assert + result.Should().Be(0); + } + + [Test] + public void Find_LargeGraph_FindsAllPoints() + { + // Arrange: Large chain + var vertices = Enumerable.Range(1, 10).ToArray(); + IEnumerable GetNeighbors(int v) + { + var neighbors = new List(); + if (v > 1) + { + neighbors.Add(v - 1); + } + + if (v < 10) + { + neighbors.Add(v + 1); + } + + return neighbors; + } + + // Act + var result = ArticulationPoints.Find(vertices, GetNeighbors); + + // Assert + result.Should().HaveCount(8); // All except endpoints + result.Should().NotContain(new[] { 1, 10 }); + } +} diff --git a/Algorithms/Graph/ArticulationPoints.cs b/Algorithms/Graph/ArticulationPoints.cs new file mode 100644 index 00000000..2239cab9 --- /dev/null +++ b/Algorithms/Graph/ArticulationPoints.cs @@ -0,0 +1,177 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Algorithms.Graph; + +/// +/// Finds articulation points (cut vertices) in an undirected graph. +/// An articulation point is a vertex whose removal increases the number of connected components. +/// +public static class ArticulationPoints +{ + /// + /// Finds all articulation points in an undirected graph. + /// + /// Type of vertex. + /// All vertices in the graph. + /// Function to get neighbors of a vertex. + /// Set of articulation points. + public static HashSet Find( + 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(); + } + + var articulationPoints = new HashSet(); + var visited = new HashSet(); + var discoveryTime = new Dictionary(); + var low = new Dictionary(); + var parent = new Dictionary(); + var time = 0; + + foreach (var vertex in vertexList) + { + if (!visited.Contains(vertex)) + { + var state = new DfsState + { + Visited = visited, + DiscoveryTime = discoveryTime, + Low = low, + Parent = parent, + ArticulationPoints = articulationPoints, + }; + Dfs(vertex, ref time, state, getNeighbors); + } + } + + return articulationPoints; + } + + /// + /// Checks if a vertex is an articulation point. + /// + /// Type of vertex. + /// Vertex to check. + /// All vertices in the graph. + /// Function to get neighbors of a vertex. + /// True if vertex is an articulation point. + public static bool IsArticulationPoint( + T vertex, + IEnumerable vertices, + Func> getNeighbors) where T : notnull + { + var articulationPoints = Find(vertices, getNeighbors); + return articulationPoints.Contains(vertex); + } + + /// + /// Counts the number of articulation points in the graph. + /// + /// Type of vertex. + /// All vertices in the graph. + /// Function to get neighbors of a vertex. + /// Number of articulation points. + public static int Count( + IEnumerable vertices, + Func> getNeighbors) where T : notnull + { + return Find(vertices, getNeighbors).Count; + } + + private static void Dfs( + T u, + ref int time, + DfsState state, + Func> getNeighbors) where T : notnull + { + state.Visited.Add(u); + state.DiscoveryTime[u] = time; + state.Low[u] = time; + time++; + + int children = 0; + + foreach (var v in getNeighbors(u)) + { + if (!state.Visited.Contains(v)) + { + children++; + state.Parent[v] = u; + Dfs(v, ref time, state, getNeighbors); + + state.Low[u] = Math.Min(state.Low[u], state.Low[v]); + + // Check if u is an articulation point + bool isRoot = !state.Parent.ContainsKey(u); + if (isRoot && children > 1) + { + state.ArticulationPoints.Add(u); + } + + bool isNonRootArticulation = state.Parent.ContainsKey(u) && state.Low[v] >= state.DiscoveryTime[u]; + if (isNonRootArticulation) + { + state.ArticulationPoints.Add(u); + } + } + else if (!EqualityComparer.Default.Equals(v, state.Parent.GetValueOrDefault(u))) + { + // Back edge: update low value + state.Low[u] = Math.Min(state.Low[u], state.DiscoveryTime[v]); + } + else + { + // Edge to parent: no action needed + } + } + } + + /// + /// Encapsulates the state for DFS traversal in articulation point detection. + /// + /// Type of vertex. + private sealed class DfsState + where T : notnull + { + /// + /// Gets set of visited vertices. + /// + public required HashSet Visited { get; init; } + + /// + /// Gets discovery time for each vertex. + /// + public required Dictionary DiscoveryTime { get; init; } + + /// + /// Gets lowest discovery time reachable from each vertex. + /// + public required Dictionary Low { get; init; } + + /// + /// Gets parent vertex in DFS tree. + /// + public required Dictionary Parent { get; init; } + + /// + /// Gets set of detected articulation points. + /// + public required HashSet ArticulationPoints { get; init; } + } +}