From 5f74d1f92b1aa7abd4054254c451fe6de9d363f7 Mon Sep 17 00:00:00 2001 From: 0xsatoshi99 <0xsatoshi99@users.noreply.github.com> Date: Thu, 13 Nov 2025 02:41:24 +0100 Subject: [PATCH 1/2] Add Bridges algorithm Finds bridges (cut edges) in undirected graphs. A bridge is an edge whose removal increases connected components. Features: - Find all bridges using DFS - Check if specific edge is a bridge - Count total bridges - O(V + E) time complexity - Works with any vertex type Tests (17 test cases): - Simple chains and triangles - Two components connected by bridge - Star graphs - Disconnected graphs - Complex graphs with cycles - Edge cases and validation Use Cases: - Network reliability analysis - Critical connection identification - Infrastructure vulnerability assessment Files: - Algorithms/Graph/Bridges.cs (177 lines) - Algorithms.Tests/Graph/BridgesTests.cs (326 lines) --- Algorithms.Tests/Graph/BridgesTests.cs | 346 +++++++++++++++++++++++++ Algorithms/Graph/Bridges.cs | 169 ++++++++++++ 2 files changed, 515 insertions(+) create mode 100644 Algorithms.Tests/Graph/BridgesTests.cs create mode 100644 Algorithms/Graph/Bridges.cs diff --git a/Algorithms.Tests/Graph/BridgesTests.cs b/Algorithms.Tests/Graph/BridgesTests.cs new file mode 100644 index 00000000..b8c572e6 --- /dev/null +++ b/Algorithms.Tests/Graph/BridgesTests.cs @@ -0,0 +1,346 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Algorithms.Graph; +using FluentAssertions; +using NUnit.Framework; + +namespace Algorithms.Tests.Graph; + +public class BridgesTests +{ + [Test] + public void Find_SimpleChain_ReturnsAllEdges() + { + // Arrange: A - B - C (both edges are bridges) + 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 = Bridges.Find(vertices, GetNeighbors); + + // Assert + result.Should().HaveCount(2); + result.Should().Contain(new[] { ("A", "B"), ("B", "C") }); + } + + [Test] + public void Find_Triangle_ReturnsEmpty() + { + // Arrange: A - B - C - A (no bridges in 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 = Bridges.Find(vertices, GetNeighbors); + + // Assert + result.Should().BeEmpty(); + } + + [Test] + public void Find_TwoComponentsConnectedByBridge_ReturnsBridge() + { + // 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", "C" }, + "B" => new[] { "A", "C", "D" }, + "C" => new[] { "A", "B" }, + "D" => new[] { "B", "E" }, + "E" => new[] { "D", "F", "G" }, + "F" => new[] { "E", "G" }, + "G" => new[] { "E", "F" }, + _ => Array.Empty(), + }; + + // Act + var result = Bridges.Find(vertices, GetNeighbors); + + // Assert + result.Should().HaveCount(2); + result.Should().Contain(new[] { ("B", "D"), ("D", "E") }); + } + + [Test] + public void Find_StarGraph_ReturnsAllEdges() + { + // 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 = Bridges.Find(vertices, GetNeighbors); + + // Assert + result.Should().HaveCount(3); + result.Should().Contain(new[] { ("A", "B"), ("A", "C"), ("A", "D") }); + } + + [Test] + public void Find_DisconnectedGraph_FindsBridgesInEachComponent() + { + // 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 = Bridges.Find(vertices, GetNeighbors); + + // Assert + result.Should().HaveCount(4); + result.Should().Contain(new[] { ("A", "B"), ("B", "C"), ("D", "E"), ("E", "F") }); + } + + [Test] + public void Find_SingleVertex_ReturnsEmpty() + { + // Arrange + var vertices = new[] { "A" }; + IEnumerable GetNeighbors(string v) => Array.Empty(); + + // Act + var result = Bridges.Find(vertices, GetNeighbors); + + // Assert + result.Should().BeEmpty(); + } + + [Test] + public void Find_TwoVertices_ReturnsBridge() + { + // 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 = Bridges.Find(vertices, GetNeighbors); + + // Assert + result.Should().ContainSingle(); + result.Should().Contain(("A", "B")); + } + + [Test] + public void Find_ComplexGraph_ReturnsCorrectBridges() + { + // Arrange: Complex graph with cycles and bridges + 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 = Bridges.Find(vertices, GetNeighbors); + + // Assert + result.Should().Contain(new[] { (3, 4), (6, 7) }); + } + + [Test] + public void Find_EmptyGraph_ReturnsEmpty() + { + // Arrange + var vertices = Array.Empty(); + IEnumerable GetNeighbors(string v) => Array.Empty(); + + // Act + var result = Bridges.Find(vertices, GetNeighbors); + + // Assert + result.Should().BeEmpty(); + } + + [Test] + public void Find_NullVertices_ThrowsArgumentNullException() + { + // Act + Action act = () => Bridges.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 = () => Bridges.Find(vertices, null!); + + // Assert + act.Should().Throw().WithParameterName("getNeighbors"); + } + + [Test] + public void IsBridge_ValidBridge_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 = Bridges.IsBridge("A", "B", vertices, GetNeighbors); + + // Assert + result.Should().BeTrue(); + } + + [Test] + public void IsBridge_ReverseEdge_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 = Bridges.IsBridge("B", "A", vertices, GetNeighbors); + + // Assert + result.Should().BeTrue(); + } + + [Test] + public void IsBridge_NotBridge_ReturnsFalse() + { + // Arrange: A - B - C - A (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 = Bridges.IsBridge("A", "B", vertices, GetNeighbors); + + // Assert + result.Should().BeFalse(); + } + + [Test] + public void Count_SimpleChain_ReturnsTwo() + { + // 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 = Bridges.Count(vertices, GetNeighbors); + + // Assert + result.Should().Be(2); + } + + [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 = Bridges.Count(vertices, GetNeighbors); + + // Assert + result.Should().Be(0); + } + + [Test] + public void Find_LargeChain_FindsAllBridges() + { + // 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 = Bridges.Find(vertices, GetNeighbors); + + // Assert + result.Should().HaveCount(9); // All edges in chain are bridges + } +} diff --git a/Algorithms/Graph/Bridges.cs b/Algorithms/Graph/Bridges.cs new file mode 100644 index 00000000..df9fd538 --- /dev/null +++ b/Algorithms/Graph/Bridges.cs @@ -0,0 +1,169 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Algorithms.Graph; + +/// +/// Finds bridges (cut edges) in an undirected graph. +/// A bridge is an edge whose removal increases the number of connected components. +/// +public static class Bridges +{ + /// + /// Finds all bridges in an undirected graph. + /// + /// Type of vertex. + /// All vertices in the graph. + /// Function to get neighbors of a vertex. + /// Set of bridges as tuples of vertices. + public static HashSet<(T From, T To)> 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<(T, T)>(); + } + + var bridges = new HashSet<(T, T)>(); + 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, + Bridges = bridges, + }; + Dfs(vertex, ref time, state, getNeighbors); + } + } + + return bridges; + } + + /// + /// Checks if an edge is a bridge. + /// + /// Type of vertex. + /// Source vertex. + /// Destination vertex. + /// All vertices in the graph. + /// Function to get neighbors of a vertex. + /// True if edge is a bridge. + public static bool IsBridge( + T from, + T to, + IEnumerable vertices, + Func> getNeighbors) where T : notnull + { + var bridges = Find(vertices, getNeighbors); + return bridges.Contains((from, to)) || bridges.Contains((to, from)); + } + + /// + /// Counts the number of bridges in the graph. + /// + /// Type of vertex. + /// All vertices in the graph. + /// Function to get neighbors of a vertex. + /// Number of bridges. + 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++; + + foreach (var v in getNeighbors(u)) + { + if (!state.Visited.Contains(v)) + { + state.Parent[v] = u; + Dfs(v, ref time, state, getNeighbors); + + state.Low[u] = Math.Min(state.Low[u], state.Low[v]); + + // Check if edge u-v is a bridge + if (state.Low[v] > state.DiscoveryTime[u]) + { + state.Bridges.Add((u, v)); + } + } + 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 bridge 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 bridges. + /// + public required HashSet<(T, T)> Bridges { get; init; } + } +} From d54bebcba7890498bd58e68e7700d9217562033a Mon Sep 17 00:00:00 2001 From: 0xsatoshi99 <0xsatoshi99@users.noreply.github.com> Date: Thu, 13 Nov 2025 03:03:27 +0100 Subject: [PATCH 2/2] Fix SA1414: Add tuple element names to Bridges property --- Algorithms/Graph/Bridges.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Algorithms/Graph/Bridges.cs b/Algorithms/Graph/Bridges.cs index df9fd538..ccf740e2 100644 --- a/Algorithms/Graph/Bridges.cs +++ b/Algorithms/Graph/Bridges.cs @@ -164,6 +164,6 @@ private sealed class DfsState /// /// Gets set of detected bridges. /// - public required HashSet<(T, T)> Bridges { get; init; } + public required HashSet<(T From, T To)> Bridges { get; init; } } }