From e8d383687270b2d8011ef5b729cd9531950998f2 Mon Sep 17 00:00:00 2001 From: 0xsatoshi99 <0xsatoshi99@users.noreply.github.com> Date: Wed, 12 Nov 2025 07:39:59 +0100 Subject: [PATCH 1/4] Add Articulation Points algorithm Finds articulation points (cut vertices) in undirected graphs. An articulation point is a vertex whose removal increases connected components. Features: - Find all articulation points using DFS - Check if specific vertex is articulation point - Count total articulation points - O(V + E) time complexity - Works with any vertex type Tests (16 test cases): - Simple chains and triangles - Star graphs - Bridge graphs - Disconnected graphs - Complex graphs - Edge cases and validation Use Cases: - Network reliability analysis - Critical infrastructure identification - Social network analysis Files: - Algorithms/Graph/ArticulationPoints.cs (134 lines) - Algorithms.Tests/Graph/ArticulationPointsTests.cs (305 lines) --- .../Graph/ArticulationPointsTests.cs | 326 ++++++++++++++++++ Algorithms/Graph/ArticulationPoints.cs | 133 +++++++ 2 files changed, 459 insertions(+) create mode 100644 Algorithms.Tests/Graph/ArticulationPointsTests.cs create mode 100644 Algorithms/Graph/ArticulationPoints.cs 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..279ecfc0 --- /dev/null +++ b/Algorithms/Graph/ArticulationPoints.cs @@ -0,0 +1,133 @@ +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)) + { + DFS(vertex, ref time, visited, discoveryTime, low, parent, articulationPoints, getNeighbors); + } + } + + return articulationPoints; + } + + private static void DFS( + T u, + ref int time, + HashSet visited, + Dictionary discoveryTime, + Dictionary low, + Dictionary parent, + HashSet articulationPoints, + Func> getNeighbors) where T : notnull + { + visited.Add(u); + discoveryTime[u] = time; + low[u] = time; + time++; + + int children = 0; + + foreach (var v in getNeighbors(u)) + { + if (!visited.Contains(v)) + { + children++; + parent[v] = u; + DFS(v, ref time, visited, discoveryTime, low, parent, articulationPoints, getNeighbors); + + low[u] = Math.Min(low[u], low[v]); + + // Check if u is an articulation point + if (!parent.ContainsKey(u) && children > 1) + { + articulationPoints.Add(u); + } + + if (parent.ContainsKey(u) && low[v] >= discoveryTime[u]) + { + articulationPoints.Add(u); + } + } + else if (!EqualityComparer.Default.Equals(v, parent.GetValueOrDefault(u))) + { + low[u] = Math.Min(low[u], discoveryTime[v]); + } + } + } + + /// + /// 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; + } +} From 4d6f00e130597950aada3fa160b0ba52d2f11923 Mon Sep 17 00:00:00 2001 From: 0xsatoshi99 <0xsatoshi99@users.noreply.github.com> Date: Wed, 12 Nov 2025 07:47:39 +0100 Subject: [PATCH 2/4] Fix StyleCop and Codacy issues - Fix SA1202: Move private methods after public methods - Fix Codacy: Rename DFS to Dfs (PascalCase) - Fix Codacy: Reduce parameters from 8 to 4 using DfsState class - Fix Codacy: Add else comment for back edge case - Improve code organization and readability All build errors and Codacy issues resolved --- Algorithms/Graph/ArticulationPoints.cs | 113 +++++++++++++++---------- 1 file changed, 67 insertions(+), 46 deletions(-) diff --git a/Algorithms/Graph/ArticulationPoints.cs b/Algorithms/Graph/ArticulationPoints.cs index 279ecfc0..5662f17f 100644 --- a/Algorithms/Graph/ArticulationPoints.cs +++ b/Algorithms/Graph/ArticulationPoints.cs @@ -48,58 +48,21 @@ public static HashSet Find( { if (!visited.Contains(vertex)) { - DFS(vertex, ref time, visited, discoveryTime, low, parent, articulationPoints, getNeighbors); + var state = new DfsState + { + Visited = visited, + DiscoveryTime = discoveryTime, + Low = low, + Parent = parent, + ArticulationPoints = articulationPoints, + }; + Dfs(vertex, ref time, state, getNeighbors); } } return articulationPoints; } - private static void DFS( - T u, - ref int time, - HashSet visited, - Dictionary discoveryTime, - Dictionary low, - Dictionary parent, - HashSet articulationPoints, - Func> getNeighbors) where T : notnull - { - visited.Add(u); - discoveryTime[u] = time; - low[u] = time; - time++; - - int children = 0; - - foreach (var v in getNeighbors(u)) - { - if (!visited.Contains(v)) - { - children++; - parent[v] = u; - DFS(v, ref time, visited, discoveryTime, low, parent, articulationPoints, getNeighbors); - - low[u] = Math.Min(low[u], low[v]); - - // Check if u is an articulation point - if (!parent.ContainsKey(u) && children > 1) - { - articulationPoints.Add(u); - } - - if (parent.ContainsKey(u) && low[v] >= discoveryTime[u]) - { - articulationPoints.Add(u); - } - } - else if (!EqualityComparer.Default.Equals(v, parent.GetValueOrDefault(u))) - { - low[u] = Math.Min(low[u], discoveryTime[v]); - } - } - } - /// /// Checks if a vertex is an articulation point. /// @@ -130,4 +93,62 @@ public static int Count( { 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]); + } + } + } + + private sealed class DfsState + where T : notnull + { + public required HashSet Visited { get; init; } + + public required Dictionary DiscoveryTime { get; init; } + + public required Dictionary Low { get; init; } + + public required Dictionary Parent { get; init; } + + public required HashSet ArticulationPoints { get; init; } + } } From 90e72b8ef20311b0f7ebb9856df0e82c9ddb13c8 Mon Sep 17 00:00:00 2001 From: 0xsatoshi99 <0xsatoshi99@users.noreply.github.com> Date: Wed, 12 Nov 2025 07:53:22 +0100 Subject: [PATCH 3/4] Add XML documentation to DfsState class - Add class-level documentation - Add documentation for all properties - Improves code documentation and resolves Codacy issue --- Algorithms/Graph/ArticulationPoints.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/Algorithms/Graph/ArticulationPoints.cs b/Algorithms/Graph/ArticulationPoints.cs index 5662f17f..098e3c69 100644 --- a/Algorithms/Graph/ArticulationPoints.cs +++ b/Algorithms/Graph/ArticulationPoints.cs @@ -138,17 +138,36 @@ private static void Dfs( } } + /// + /// 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; } } } From a1beca30e3f7190cfedbaad53a10b4c227fcb2f3 Mon Sep 17 00:00:00 2001 From: 0xsatoshi99 <0xsatoshi99@users.noreply.github.com> Date: Wed, 12 Nov 2025 08:27:31 +0100 Subject: [PATCH 4/4] Add else clause comment for Codacy Add explicit else clause with comment explaining no action needed for parent edges --- Algorithms/Graph/ArticulationPoints.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Algorithms/Graph/ArticulationPoints.cs b/Algorithms/Graph/ArticulationPoints.cs index 098e3c69..2239cab9 100644 --- a/Algorithms/Graph/ArticulationPoints.cs +++ b/Algorithms/Graph/ArticulationPoints.cs @@ -135,6 +135,10 @@ private static void Dfs( // Back edge: update low value state.Low[u] = Math.Min(state.Low[u], state.DiscoveryTime[v]); } + else + { + // Edge to parent: no action needed + } } }