Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
189 changes: 189 additions & 0 deletions Algorithms.Tests/Graph/TarjanStronglyConnectedComponentsTests.cs
Original file line number Diff line number Diff line change
@@ -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<ArgumentOutOfRangeException>();
}

[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);
}
}
156 changes: 156 additions & 0 deletions Algorithms/Graph/TarjanStronglyConnectedComponents.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
using System;
using System.Collections.Generic;
using System.Linq;

namespace Algorithms.Graph;

/// <summary>
/// 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.
/// </summary>
public class TarjanStronglyConnectedComponents
{
private readonly List<int>[] graph;
private readonly int[] ids;
private readonly int[] low;
private readonly bool[] onStack;
private readonly Stack<int> stack;
private readonly List<List<int>> sccs;
private int id;

public TarjanStronglyConnectedComponents(int vertices)
{
graph = new List<int>[vertices];
ids = new int[vertices];
low = new int[vertices];
onStack = new bool[vertices];
stack = new Stack<int>();
sccs = new List<List<int>>();

for (int i = 0; i < vertices; i++)
{
graph[i] = new List<int>();
ids[i] = -1;
}
}

/// <summary>
/// Adds a directed edge from u to v.
/// </summary>
public void AddEdge(int u, int v)
{
if (u < 0 || u >= graph.Length || v < 0 || v >= graph.Length)

Check notice on line 42 in Algorithms/Graph/TarjanStronglyConnectedComponents.cs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

Algorithms/Graph/TarjanStronglyConnectedComponents.cs#L42

Add curly braces around the nested statement(s) in this 'if' block.
throw new ArgumentOutOfRangeException();

Check warning on line 43 in Algorithms/Graph/TarjanStronglyConnectedComponents.cs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

Algorithms/Graph/TarjanStronglyConnectedComponents.cs#L43

Use a constructor overloads that allows a more meaningful exception message to be provided.

Check failure on line 43 in Algorithms/Graph/TarjanStronglyConnectedComponents.cs

View workflow job for this annotation

GitHub Actions / build


graph[u].Add(v);
}

/// <summary>
/// Finds all strongly connected components.
/// </summary>
/// <returns>List of SCCs, where each SCC is a list of vertex indices.</returns>
public List<List<int>> FindSCCs()
{
for (int i = 0; i < graph.Length; i++)
{
if (ids[i] == -1)

Check notice on line 56 in Algorithms/Graph/TarjanStronglyConnectedComponents.cs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

Algorithms/Graph/TarjanStronglyConnectedComponents.cs#L56

Add curly braces around the nested statement(s) in this 'if' block.
Dfs(i);

Check failure on line 57 in Algorithms/Graph/TarjanStronglyConnectedComponents.cs

View workflow job for this annotation

GitHub Actions / build

}

return sccs;
}

/// <summary>
/// Gets the number of strongly connected components.
/// </summary>
public int GetSCCCount() => sccs.Count;

Check notice on line 66 in Algorithms/Graph/TarjanStronglyConnectedComponents.cs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

Algorithms/Graph/TarjanStronglyConnectedComponents.cs#L66

Rename method 'GetSCCCount' to match pascal case naming rules, consider using 'GetSccCount'.

/// <summary>
/// Checks if two vertices are in the same SCC.
/// </summary>
public bool InSameSCC(int u, int v)

Check notice on line 71 in Algorithms/Graph/TarjanStronglyConnectedComponents.cs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

Algorithms/Graph/TarjanStronglyConnectedComponents.cs#L71

Rename method 'InSameSCC' to match pascal case naming rules, consider using 'InSameScc'.
{
if (sccs.Count == 0) FindSCCs();

Check notice on line 73 in Algorithms/Graph/TarjanStronglyConnectedComponents.cs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

Algorithms/Graph/TarjanStronglyConnectedComponents.cs#L73

Add curly braces around the nested statement(s) in this 'if' block.

Check notice on line 73 in Algorithms/Graph/TarjanStronglyConnectedComponents.cs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

Algorithms/Graph/TarjanStronglyConnectedComponents.cs#L73

Reformat the code to have only one statement per line.

Check failure on line 73 in Algorithms/Graph/TarjanStronglyConnectedComponents.cs

View workflow job for this annotation

GitHub Actions / build


foreach (var scc in sccs)
{
if (scc.Contains(u) && scc.Contains(v))

Check notice on line 77 in Algorithms/Graph/TarjanStronglyConnectedComponents.cs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

Algorithms/Graph/TarjanStronglyConnectedComponents.cs#L77

Add curly braces around the nested statement(s) in this 'if' block.
return true;

Check failure on line 78 in Algorithms/Graph/TarjanStronglyConnectedComponents.cs

View workflow job for this annotation

GitHub Actions / build

}

return false;
}

/// <summary>
/// Gets the SCC containing the given vertex.
/// </summary>
public List<int>? GetSCC(int vertex)

Check notice on line 87 in Algorithms/Graph/TarjanStronglyConnectedComponents.cs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

Algorithms/Graph/TarjanStronglyConnectedComponents.cs#L87

Rename method 'GetSCC' to match pascal case naming rules, consider using 'GetScc'.
{
if (sccs.Count == 0) FindSCCs();

Check failure on line 89 in Algorithms/Graph/TarjanStronglyConnectedComponents.cs

View workflow job for this annotation

GitHub Actions / build


return sccs.FirstOrDefault(scc => scc.Contains(vertex));
}

/// <summary>
/// Builds the condensation graph (DAG of SCCs).
/// </summary>
public List<int>[] BuildCondensationGraph()
{
if (sccs.Count == 0) FindSCCs();

Check failure on line 99 in Algorithms/Graph/TarjanStronglyConnectedComponents.cs

View workflow job for this annotation

GitHub Actions / build


var sccIndex = new int[graph.Length];
for (int i = 0; i < sccs.Count; i++)
{
foreach (var vertex in sccs[i])

Check notice on line 104 in Algorithms/Graph/TarjanStronglyConnectedComponents.cs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

Algorithms/Graph/TarjanStronglyConnectedComponents.cs#L104

Add curly braces around the nested statement(s) in this 'foreach' block.
sccIndex[vertex] = i;

Check failure on line 105 in Algorithms/Graph/TarjanStronglyConnectedComponents.cs

View workflow job for this annotation

GitHub Actions / build

}

var condensation = new List<int>[sccs.Count];
for (int i = 0; i < sccs.Count; i++)

Check notice on line 109 in Algorithms/Graph/TarjanStronglyConnectedComponents.cs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

Algorithms/Graph/TarjanStronglyConnectedComponents.cs#L109

Add curly braces around the nested statement(s) in this 'for' block.
condensation[i] = new List<int>();

Check failure on line 110 in Algorithms/Graph/TarjanStronglyConnectedComponents.cs

View workflow job for this annotation

GitHub Actions / build


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);

Check notice on line 139 in Algorithms/Graph/TarjanStronglyConnectedComponents.cs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

Algorithms/Graph/TarjanStronglyConnectedComponents.cs#L139

Add curly braces around the nested statement(s) in this 'if' block.

Check notice on line 139 in Algorithms/Graph/TarjanStronglyConnectedComponents.cs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

Algorithms/Graph/TarjanStronglyConnectedComponents.cs#L139

Reformat the code to have only one statement per line.

Check failure on line 139 in Algorithms/Graph/TarjanStronglyConnectedComponents.cs

View workflow job for this annotation

GitHub Actions / build

if (onStack[to]) low[at] = Math.Min(low[at], low[to]);

Check notice on line 140 in Algorithms/Graph/TarjanStronglyConnectedComponents.cs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

Algorithms/Graph/TarjanStronglyConnectedComponents.cs#L140

Add curly braces around the nested statement(s) in this 'if' block.

Check notice on line 140 in Algorithms/Graph/TarjanStronglyConnectedComponents.cs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

Algorithms/Graph/TarjanStronglyConnectedComponents.cs#L140

Reformat the code to have only one statement per line.

Check failure on line 140 in Algorithms/Graph/TarjanStronglyConnectedComponents.cs

View workflow job for this annotation

GitHub Actions / build

}

if (ids[at] == low[at])
{
var scc = new List<int>();
while (true)
{
int node = stack.Pop();
onStack[node] = false;
scc.Add(node);
if (node == at) break;

Check notice on line 151 in Algorithms/Graph/TarjanStronglyConnectedComponents.cs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

Algorithms/Graph/TarjanStronglyConnectedComponents.cs#L151

Add curly braces around the nested statement(s) in this 'if' block.

Check notice on line 151 in Algorithms/Graph/TarjanStronglyConnectedComponents.cs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

Algorithms/Graph/TarjanStronglyConnectedComponents.cs#L151

Reformat the code to have only one statement per line.
}
sccs.Add(scc);
}
}
}