From 61e5f1c2af86f70f33b7d6281092cfcf2f94edea Mon Sep 17 00:00:00 2001 From: 0xsatoshi99 <0xsatoshi99@users.noreply.github.com> Date: Thu, 6 Nov 2025 01:58:09 +0100 Subject: [PATCH 1/4] Add Tarjan's strongly connected components algorithm --- .../TarjanStronglyConnectedComponentsTests.cs | 189 ++++++++++++++++++ .../TarjanStronglyConnectedComponents.cs | 156 +++++++++++++++ 2 files changed, 345 insertions(+) create mode 100644 Algorithms.Tests/Graph/TarjanStronglyConnectedComponentsTests.cs create mode 100644 Algorithms/Graph/TarjanStronglyConnectedComponents.cs diff --git a/Algorithms.Tests/Graph/TarjanStronglyConnectedComponentsTests.cs b/Algorithms.Tests/Graph/TarjanStronglyConnectedComponentsTests.cs new file mode 100644 index 00000000..bb4c4312 --- /dev/null +++ b/Algorithms.Tests/Graph/TarjanStronglyConnectedComponentsTests.cs @@ -0,0 +1,189 @@ +using Algorithms.Graph; +using NUnit.Framework; +using FluentAssertions; +using System.Linq; + +namespace Algorithms.Tests.Graph; + +public class TarjanStronglyConnectedComponentsTests +{ + [Test] + public void FindSCCs_SimpleGraph_ReturnsCorrectSCCs() + { + var tarjan = new TarjanStronglyConnectedComponents(3); + tarjan.AddEdge(0, 1); + tarjan.AddEdge(1, 2); + tarjan.AddEdge(2, 0); + + var sccs = tarjan.FindSCCs(); + + sccs.Should().HaveCount(1); + sccs[0].Should().BeEquivalentTo(new[] { 0, 1, 2 }); + } + + [Test] + public void FindSCCs_TwoSeparateSCCs_ReturnsBothSCCs() + { + var tarjan = new TarjanStronglyConnectedComponents(4); + tarjan.AddEdge(0, 1); + tarjan.AddEdge(1, 0); + tarjan.AddEdge(2, 3); + tarjan.AddEdge(3, 2); + + var sccs = tarjan.FindSCCs(); + + sccs.Should().HaveCount(2); + } + + [Test] + public void FindSCCs_DisconnectedVertices_EachVertexIsSCC() + { + var tarjan = new TarjanStronglyConnectedComponents(3); + + var sccs = tarjan.FindSCCs(); + + sccs.Should().HaveCount(3); + sccs.Should().OnlyContain(scc => scc.Count == 1); + } + + [Test] + public void FindSCCs_ComplexGraph_ReturnsCorrectSCCs() + { + var tarjan = new TarjanStronglyConnectedComponents(8); + tarjan.AddEdge(0, 1); + tarjan.AddEdge(1, 2); + tarjan.AddEdge(2, 0); + tarjan.AddEdge(2, 3); + tarjan.AddEdge(3, 4); + tarjan.AddEdge(4, 5); + tarjan.AddEdge(5, 3); + tarjan.AddEdge(5, 6); + tarjan.AddEdge(6, 7); + tarjan.AddEdge(7, 6); + + var sccs = tarjan.FindSCCs(); + + sccs.Should().HaveCount(3); + } + + [Test] + public void GetSCCCount_AfterFindingSCCs_ReturnsCorrectCount() + { + var tarjan = new TarjanStronglyConnectedComponents(5); + tarjan.AddEdge(0, 1); + tarjan.AddEdge(1, 0); + tarjan.AddEdge(2, 3); + tarjan.AddEdge(3, 4); + tarjan.AddEdge(4, 2); + + tarjan.FindSCCs(); + var count = tarjan.GetSCCCount(); + + count.Should().Be(2); + } + + [Test] + public void InSameSCC_VerticesInSameSCC_ReturnsTrue() + { + var tarjan = new TarjanStronglyConnectedComponents(3); + tarjan.AddEdge(0, 1); + tarjan.AddEdge(1, 2); + tarjan.AddEdge(2, 0); + + var result = tarjan.InSameSCC(0, 2); + + result.Should().BeTrue(); + } + + [Test] + public void InSameSCC_VerticesInDifferentSCCs_ReturnsFalse() + { + var tarjan = new TarjanStronglyConnectedComponents(4); + tarjan.AddEdge(0, 1); + tarjan.AddEdge(1, 0); + tarjan.AddEdge(2, 3); + + var result = tarjan.InSameSCC(0, 2); + + result.Should().BeFalse(); + } + + [Test] + public void GetSCC_ValidVertex_ReturnsSCCContainingVertex() + { + var tarjan = new TarjanStronglyConnectedComponents(3); + tarjan.AddEdge(0, 1); + tarjan.AddEdge(1, 2); + tarjan.AddEdge(2, 0); + + var scc = tarjan.GetSCC(1); + + scc.Should().NotBeNull(); + scc.Should().Contain(1); + scc.Should().HaveCount(3); + } + + [Test] + public void BuildCondensationGraph_ComplexGraph_ReturnsDAG() + { + var tarjan = new TarjanStronglyConnectedComponents(6); + tarjan.AddEdge(0, 1); + tarjan.AddEdge(1, 0); + tarjan.AddEdge(1, 2); + tarjan.AddEdge(2, 3); + tarjan.AddEdge(3, 4); + tarjan.AddEdge(4, 5); + tarjan.AddEdge(5, 3); + + var condensation = tarjan.BuildCondensationGraph(); + + condensation.Should().NotBeNull(); + condensation.Length.Should().Be(2); + } + + [Test] + public void AddEdge_InvalidVertex_ThrowsException() + { + var tarjan = new TarjanStronglyConnectedComponents(3); + + var act = () => tarjan.AddEdge(0, 5); + + act.Should().Throw(); + } + + [Test] + public void FindSCCs_SingleVertex_ReturnsSingleSCC() + { + var tarjan = new TarjanStronglyConnectedComponents(1); + + var sccs = tarjan.FindSCCs(); + + sccs.Should().HaveCount(1); + sccs[0].Should().BeEquivalentTo(new[] { 0 }); + } + + [Test] + public void FindSCCs_LinearChain_EachVertexIsSCC() + { + var tarjan = new TarjanStronglyConnectedComponents(4); + tarjan.AddEdge(0, 1); + tarjan.AddEdge(1, 2); + tarjan.AddEdge(2, 3); + + var sccs = tarjan.FindSCCs(); + + sccs.Should().HaveCount(4); + } + + [Test] + public void FindSCCs_SelfLoop_VertexIsSCC() + { + var tarjan = new TarjanStronglyConnectedComponents(2); + tarjan.AddEdge(0, 0); + tarjan.AddEdge(0, 1); + + var sccs = tarjan.FindSCCs(); + + sccs.Should().Contain(scc => scc.Contains(0) && scc.Count == 1); + } +} diff --git a/Algorithms/Graph/TarjanStronglyConnectedComponents.cs b/Algorithms/Graph/TarjanStronglyConnectedComponents.cs new file mode 100644 index 00000000..0994745e --- /dev/null +++ b/Algorithms/Graph/TarjanStronglyConnectedComponents.cs @@ -0,0 +1,156 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Algorithms.Graph; + +/// +/// Tarjan's algorithm for finding strongly connected components in a directed graph. +/// Uses depth-first search with a stack to identify SCCs in O(V + E) time. +/// +public class TarjanStronglyConnectedComponents +{ + private readonly List[] graph; + private readonly int[] ids; + private readonly int[] low; + private readonly bool[] onStack; + private readonly Stack stack; + private readonly List> sccs; + private int id; + + public TarjanStronglyConnectedComponents(int vertices) + { + graph = new List[vertices]; + ids = new int[vertices]; + low = new int[vertices]; + onStack = new bool[vertices]; + stack = new Stack(); + sccs = new List>(); + + for (int i = 0; i < vertices; i++) + { + graph[i] = new List(); + ids[i] = -1; + } + } + + /// + /// Adds a directed edge from u to v. + /// + public void AddEdge(int u, int v) + { + if (u < 0 || u >= graph.Length || v < 0 || v >= graph.Length) + throw new ArgumentOutOfRangeException(); + + graph[u].Add(v); + } + + /// + /// Finds all strongly connected components. + /// + /// List of SCCs, where each SCC is a list of vertex indices. + public List> FindSCCs() + { + for (int i = 0; i < graph.Length; i++) + { + if (ids[i] == -1) + Dfs(i); + } + + return sccs; + } + + /// + /// Gets the number of strongly connected components. + /// + public int GetSCCCount() => sccs.Count; + + /// + /// Checks if two vertices are in the same SCC. + /// + public bool InSameSCC(int u, int v) + { + if (sccs.Count == 0) FindSCCs(); + + foreach (var scc in sccs) + { + if (scc.Contains(u) && scc.Contains(v)) + return true; + } + + return false; + } + + /// + /// Gets the SCC containing the given vertex. + /// + public List? GetSCC(int vertex) + { + if (sccs.Count == 0) FindSCCs(); + + return sccs.FirstOrDefault(scc => scc.Contains(vertex)); + } + + /// + /// Builds the condensation graph (DAG of SCCs). + /// + public List[] BuildCondensationGraph() + { + if (sccs.Count == 0) FindSCCs(); + + var sccIndex = new int[graph.Length]; + for (int i = 0; i < sccs.Count; i++) + { + foreach (var vertex in sccs[i]) + sccIndex[vertex] = i; + } + + var condensation = new List[sccs.Count]; + for (int i = 0; i < sccs.Count; i++) + condensation[i] = new List(); + + var edges = new HashSet<(int, int)>(); + for (int u = 0; u < graph.Length; u++) + { + foreach (var v in graph[u]) + { + int sccU = sccIndex[u]; + int sccV = sccIndex[v]; + + if (sccU != sccV && !edges.Contains((sccU, sccV))) + { + condensation[sccU].Add(sccV); + edges.Add((sccU, sccV)); + } + } + } + + return condensation; + } + + private void Dfs(int at) + { + stack.Push(at); + onStack[at] = true; + ids[at] = low[at] = id++; + + foreach (var to in graph[at]) + { + if (ids[to] == -1) Dfs(to); + if (onStack[to]) low[at] = Math.Min(low[at], low[to]); + } + + if (ids[at] == low[at]) + { + var scc = new List(); + while (true) + { + int node = stack.Pop(); + onStack[node] = false; + scc.Add(node); + if (node == at) break; + } + sccs.Add(scc); + } + } +} From 92a394560cf35733a9459851fe314575b50face9 Mon Sep 17 00:00:00 2001 From: 0xsatoshi99 <0xsatoshi99@users.noreply.github.com> Date: Mon, 10 Nov 2025 09:00:41 +0100 Subject: [PATCH 2/4] Fix all 17 Codacy static analysis issues - Add curly braces to all single-statement blocks - Fix naming conventions: GetSCCCount -> GetSccCount, InSameSCC -> InSameScc, GetSCC -> GetScc - Add meaningful exception message to ArgumentOutOfRangeException - Split multi-statement lines into separate lines - Update all test method names to match renamed methods --- .../TarjanStronglyConnectedComponentsTests.cs | 16 +++--- .../TarjanStronglyConnectedComponents.cs | 49 +++++++++++++++---- 2 files changed, 47 insertions(+), 18 deletions(-) diff --git a/Algorithms.Tests/Graph/TarjanStronglyConnectedComponentsTests.cs b/Algorithms.Tests/Graph/TarjanStronglyConnectedComponentsTests.cs index bb4c4312..c493b339 100644 --- a/Algorithms.Tests/Graph/TarjanStronglyConnectedComponentsTests.cs +++ b/Algorithms.Tests/Graph/TarjanStronglyConnectedComponentsTests.cs @@ -67,7 +67,7 @@ public void FindSCCs_ComplexGraph_ReturnsCorrectSCCs() } [Test] - public void GetSCCCount_AfterFindingSCCs_ReturnsCorrectCount() + public void GetSccCount_AfterFindingSCCs_ReturnsCorrectCount() { var tarjan = new TarjanStronglyConnectedComponents(5); tarjan.AddEdge(0, 1); @@ -77,46 +77,46 @@ public void GetSCCCount_AfterFindingSCCs_ReturnsCorrectCount() tarjan.AddEdge(4, 2); tarjan.FindSCCs(); - var count = tarjan.GetSCCCount(); + var count = tarjan.GetSccCount(); count.Should().Be(2); } [Test] - public void InSameSCC_VerticesInSameSCC_ReturnsTrue() + public void InSameScc_VerticesInSameScc_ReturnsTrue() { var tarjan = new TarjanStronglyConnectedComponents(3); tarjan.AddEdge(0, 1); tarjan.AddEdge(1, 2); tarjan.AddEdge(2, 0); - var result = tarjan.InSameSCC(0, 2); + var result = tarjan.InSameScc(0, 2); result.Should().BeTrue(); } [Test] - public void InSameSCC_VerticesInDifferentSCCs_ReturnsFalse() + public void InSameScc_VerticesInDifferentSccs_ReturnsFalse() { var tarjan = new TarjanStronglyConnectedComponents(4); tarjan.AddEdge(0, 1); tarjan.AddEdge(1, 0); tarjan.AddEdge(2, 3); - var result = tarjan.InSameSCC(0, 2); + var result = tarjan.InSameScc(0, 2); result.Should().BeFalse(); } [Test] - public void GetSCC_ValidVertex_ReturnsSCCContainingVertex() + public void GetScc_ValidVertex_ReturnsSccContainingVertex() { var tarjan = new TarjanStronglyConnectedComponents(3); tarjan.AddEdge(0, 1); tarjan.AddEdge(1, 2); tarjan.AddEdge(2, 0); - var scc = tarjan.GetSCC(1); + var scc = tarjan.GetScc(1); scc.Should().NotBeNull(); scc.Should().Contain(1); diff --git a/Algorithms/Graph/TarjanStronglyConnectedComponents.cs b/Algorithms/Graph/TarjanStronglyConnectedComponents.cs index 0994745e..ecd696ba 100644 --- a/Algorithms/Graph/TarjanStronglyConnectedComponents.cs +++ b/Algorithms/Graph/TarjanStronglyConnectedComponents.cs @@ -40,7 +40,9 @@ public TarjanStronglyConnectedComponents(int vertices) public void AddEdge(int u, int v) { if (u < 0 || u >= graph.Length || v < 0 || v >= graph.Length) - throw new ArgumentOutOfRangeException(); + { + throw new ArgumentOutOfRangeException(nameof(u), "Vertex indices must be within valid range."); + } graph[u].Add(v); } @@ -54,7 +56,9 @@ public List> FindSCCs() for (int i = 0; i < graph.Length; i++) { if (ids[i] == -1) + { Dfs(i); + } } return sccs; @@ -63,19 +67,24 @@ public List> FindSCCs() /// /// Gets the number of strongly connected components. /// - public int GetSCCCount() => sccs.Count; + public int GetSccCount() => sccs.Count; /// /// Checks if two vertices are in the same SCC. /// - public bool InSameSCC(int u, int v) + public bool InSameScc(int u, int v) { - if (sccs.Count == 0) FindSCCs(); + if (sccs.Count == 0) + { + FindSCCs(); + } foreach (var scc in sccs) { if (scc.Contains(u) && scc.Contains(v)) + { return true; + } } return false; @@ -84,9 +93,12 @@ public bool InSameSCC(int u, int v) /// /// Gets the SCC containing the given vertex. /// - public List? GetSCC(int vertex) + public List? GetScc(int vertex) { - if (sccs.Count == 0) FindSCCs(); + if (sccs.Count == 0) + { + FindSCCs(); + } return sccs.FirstOrDefault(scc => scc.Contains(vertex)); } @@ -96,18 +108,25 @@ public bool InSameSCC(int u, int v) /// public List[] BuildCondensationGraph() { - if (sccs.Count == 0) FindSCCs(); + if (sccs.Count == 0) + { + FindSCCs(); + } var sccIndex = new int[graph.Length]; for (int i = 0; i < sccs.Count; i++) { foreach (var vertex in sccs[i]) + { sccIndex[vertex] = i; + } } var condensation = new List[sccs.Count]; for (int i = 0; i < sccs.Count; i++) + { condensation[i] = new List(); + } var edges = new HashSet<(int, int)>(); for (int u = 0; u < graph.Length; u++) @@ -136,8 +155,15 @@ private void Dfs(int at) foreach (var to in graph[at]) { - if (ids[to] == -1) Dfs(to); - if (onStack[to]) low[at] = Math.Min(low[at], low[to]); + if (ids[to] == -1) + { + Dfs(to); + } + + if (onStack[to]) + { + low[at] = Math.Min(low[at], low[to]); + } } if (ids[at] == low[at]) @@ -148,7 +174,10 @@ private void Dfs(int at) int node = stack.Pop(); onStack[node] = false; scc.Add(node); - if (node == at) break; + if (node == at) + { + break; + } } sccs.Add(scc); } From 2f838fbb1880c0003450429354e4b7a2eaba346e Mon Sep 17 00:00:00 2001 From: 0xsatoshi99 <0xsatoshi99@users.noreply.github.com> Date: Mon, 10 Nov 2025 10:26:45 +0100 Subject: [PATCH 3/4] Fix StyleCop SA1513: Add blank line after closing brace --- Algorithms/Graph/TarjanStronglyConnectedComponents.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Algorithms/Graph/TarjanStronglyConnectedComponents.cs b/Algorithms/Graph/TarjanStronglyConnectedComponents.cs index ecd696ba..3f4a1f78 100644 --- a/Algorithms/Graph/TarjanStronglyConnectedComponents.cs +++ b/Algorithms/Graph/TarjanStronglyConnectedComponents.cs @@ -179,6 +179,7 @@ private void Dfs(int at) break; } } + sccs.Add(scc); } } From ac213955969a052d30c82bcda8783c52dd37df4d Mon Sep 17 00:00:00 2001 From: 0xsatoshi99 <0xsatoshi99@users.noreply.github.com> Date: Mon, 10 Nov 2025 14:27:08 +0100 Subject: [PATCH 4/4] Fix test expectation for BuildCondensationGraph MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Graph has 3 SCCs not 2: - SCC 1: {0, 1} (cycle 0→1→0) - SCC 2: {2} (single node) - SCC 3: {3, 4, 5} (cycle 3→4→5→3) --- .../Graph/TarjanStronglyConnectedComponentsTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Algorithms.Tests/Graph/TarjanStronglyConnectedComponentsTests.cs b/Algorithms.Tests/Graph/TarjanStronglyConnectedComponentsTests.cs index c493b339..913baf5f 100644 --- a/Algorithms.Tests/Graph/TarjanStronglyConnectedComponentsTests.cs +++ b/Algorithms.Tests/Graph/TarjanStronglyConnectedComponentsTests.cs @@ -138,7 +138,7 @@ public void BuildCondensationGraph_ComplexGraph_ReturnsDAG() var condensation = tarjan.BuildCondensationGraph(); condensation.Should().NotBeNull(); - condensation.Length.Should().Be(2); + condensation.Length.Should().Be(3); // {0,1}, {2}, {3,4,5} } [Test]