diff --git a/Algorithms.Tests/Graph/TarjanStronglyConnectedComponentsTests.cs b/Algorithms.Tests/Graph/TarjanStronglyConnectedComponentsTests.cs new file mode 100644 index 00000000..913baf5f --- /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(3); // {0,1}, {2}, {3,4,5} + } + + [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..3f4a1f78 --- /dev/null +++ b/Algorithms/Graph/TarjanStronglyConnectedComponents.cs @@ -0,0 +1,186 @@ +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(nameof(u), "Vertex indices must be within valid range."); + } + + 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); + } + } +}