Skip to content

Commit 5bcbece

Browse files
authored
Add Tarjan's strongly connected components algorithm (#561)
1 parent aba01fb commit 5bcbece

File tree

2 files changed

+375
-0
lines changed

2 files changed

+375
-0
lines changed
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
using Algorithms.Graph;
2+
using NUnit.Framework;
3+
using FluentAssertions;
4+
using System.Linq;
5+
6+
namespace Algorithms.Tests.Graph;
7+
8+
public class TarjanStronglyConnectedComponentsTests
9+
{
10+
[Test]
11+
public void FindSCCs_SimpleGraph_ReturnsCorrectSCCs()
12+
{
13+
var tarjan = new TarjanStronglyConnectedComponents(3);
14+
tarjan.AddEdge(0, 1);
15+
tarjan.AddEdge(1, 2);
16+
tarjan.AddEdge(2, 0);
17+
18+
var sccs = tarjan.FindSCCs();
19+
20+
sccs.Should().HaveCount(1);
21+
sccs[0].Should().BeEquivalentTo(new[] { 0, 1, 2 });
22+
}
23+
24+
[Test]
25+
public void FindSCCs_TwoSeparateSCCs_ReturnsBothSCCs()
26+
{
27+
var tarjan = new TarjanStronglyConnectedComponents(4);
28+
tarjan.AddEdge(0, 1);
29+
tarjan.AddEdge(1, 0);
30+
tarjan.AddEdge(2, 3);
31+
tarjan.AddEdge(3, 2);
32+
33+
var sccs = tarjan.FindSCCs();
34+
35+
sccs.Should().HaveCount(2);
36+
}
37+
38+
[Test]
39+
public void FindSCCs_DisconnectedVertices_EachVertexIsSCC()
40+
{
41+
var tarjan = new TarjanStronglyConnectedComponents(3);
42+
43+
var sccs = tarjan.FindSCCs();
44+
45+
sccs.Should().HaveCount(3);
46+
sccs.Should().OnlyContain(scc => scc.Count == 1);
47+
}
48+
49+
[Test]
50+
public void FindSCCs_ComplexGraph_ReturnsCorrectSCCs()
51+
{
52+
var tarjan = new TarjanStronglyConnectedComponents(8);
53+
tarjan.AddEdge(0, 1);
54+
tarjan.AddEdge(1, 2);
55+
tarjan.AddEdge(2, 0);
56+
tarjan.AddEdge(2, 3);
57+
tarjan.AddEdge(3, 4);
58+
tarjan.AddEdge(4, 5);
59+
tarjan.AddEdge(5, 3);
60+
tarjan.AddEdge(5, 6);
61+
tarjan.AddEdge(6, 7);
62+
tarjan.AddEdge(7, 6);
63+
64+
var sccs = tarjan.FindSCCs();
65+
66+
sccs.Should().HaveCount(3);
67+
}
68+
69+
[Test]
70+
public void GetSccCount_AfterFindingSCCs_ReturnsCorrectCount()
71+
{
72+
var tarjan = new TarjanStronglyConnectedComponents(5);
73+
tarjan.AddEdge(0, 1);
74+
tarjan.AddEdge(1, 0);
75+
tarjan.AddEdge(2, 3);
76+
tarjan.AddEdge(3, 4);
77+
tarjan.AddEdge(4, 2);
78+
79+
tarjan.FindSCCs();
80+
var count = tarjan.GetSccCount();
81+
82+
count.Should().Be(2);
83+
}
84+
85+
[Test]
86+
public void InSameScc_VerticesInSameScc_ReturnsTrue()
87+
{
88+
var tarjan = new TarjanStronglyConnectedComponents(3);
89+
tarjan.AddEdge(0, 1);
90+
tarjan.AddEdge(1, 2);
91+
tarjan.AddEdge(2, 0);
92+
93+
var result = tarjan.InSameScc(0, 2);
94+
95+
result.Should().BeTrue();
96+
}
97+
98+
[Test]
99+
public void InSameScc_VerticesInDifferentSccs_ReturnsFalse()
100+
{
101+
var tarjan = new TarjanStronglyConnectedComponents(4);
102+
tarjan.AddEdge(0, 1);
103+
tarjan.AddEdge(1, 0);
104+
tarjan.AddEdge(2, 3);
105+
106+
var result = tarjan.InSameScc(0, 2);
107+
108+
result.Should().BeFalse();
109+
}
110+
111+
[Test]
112+
public void GetScc_ValidVertex_ReturnsSccContainingVertex()
113+
{
114+
var tarjan = new TarjanStronglyConnectedComponents(3);
115+
tarjan.AddEdge(0, 1);
116+
tarjan.AddEdge(1, 2);
117+
tarjan.AddEdge(2, 0);
118+
119+
var scc = tarjan.GetScc(1);
120+
121+
scc.Should().NotBeNull();
122+
scc.Should().Contain(1);
123+
scc.Should().HaveCount(3);
124+
}
125+
126+
[Test]
127+
public void BuildCondensationGraph_ComplexGraph_ReturnsDAG()
128+
{
129+
var tarjan = new TarjanStronglyConnectedComponents(6);
130+
tarjan.AddEdge(0, 1);
131+
tarjan.AddEdge(1, 0);
132+
tarjan.AddEdge(1, 2);
133+
tarjan.AddEdge(2, 3);
134+
tarjan.AddEdge(3, 4);
135+
tarjan.AddEdge(4, 5);
136+
tarjan.AddEdge(5, 3);
137+
138+
var condensation = tarjan.BuildCondensationGraph();
139+
140+
condensation.Should().NotBeNull();
141+
condensation.Length.Should().Be(3); // {0,1}, {2}, {3,4,5}
142+
}
143+
144+
[Test]
145+
public void AddEdge_InvalidVertex_ThrowsException()
146+
{
147+
var tarjan = new TarjanStronglyConnectedComponents(3);
148+
149+
var act = () => tarjan.AddEdge(0, 5);
150+
151+
act.Should().Throw<ArgumentOutOfRangeException>();
152+
}
153+
154+
[Test]
155+
public void FindSCCs_SingleVertex_ReturnsSingleSCC()
156+
{
157+
var tarjan = new TarjanStronglyConnectedComponents(1);
158+
159+
var sccs = tarjan.FindSCCs();
160+
161+
sccs.Should().HaveCount(1);
162+
sccs[0].Should().BeEquivalentTo(new[] { 0 });
163+
}
164+
165+
[Test]
166+
public void FindSCCs_LinearChain_EachVertexIsSCC()
167+
{
168+
var tarjan = new TarjanStronglyConnectedComponents(4);
169+
tarjan.AddEdge(0, 1);
170+
tarjan.AddEdge(1, 2);
171+
tarjan.AddEdge(2, 3);
172+
173+
var sccs = tarjan.FindSCCs();
174+
175+
sccs.Should().HaveCount(4);
176+
}
177+
178+
[Test]
179+
public void FindSCCs_SelfLoop_VertexIsSCC()
180+
{
181+
var tarjan = new TarjanStronglyConnectedComponents(2);
182+
tarjan.AddEdge(0, 0);
183+
tarjan.AddEdge(0, 1);
184+
185+
var sccs = tarjan.FindSCCs();
186+
187+
sccs.Should().Contain(scc => scc.Contains(0) && scc.Count == 1);
188+
}
189+
}
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
5+
namespace Algorithms.Graph;
6+
7+
/// <summary>
8+
/// Tarjan's algorithm for finding strongly connected components in a directed graph.
9+
/// Uses depth-first search with a stack to identify SCCs in O(V + E) time.
10+
/// </summary>
11+
public class TarjanStronglyConnectedComponents
12+
{
13+
private readonly List<int>[] graph;
14+
private readonly int[] ids;
15+
private readonly int[] low;
16+
private readonly bool[] onStack;
17+
private readonly Stack<int> stack;
18+
private readonly List<List<int>> sccs;
19+
private int id;
20+
21+
public TarjanStronglyConnectedComponents(int vertices)
22+
{
23+
graph = new List<int>[vertices];
24+
ids = new int[vertices];
25+
low = new int[vertices];
26+
onStack = new bool[vertices];
27+
stack = new Stack<int>();
28+
sccs = new List<List<int>>();
29+
30+
for (int i = 0; i < vertices; i++)
31+
{
32+
graph[i] = new List<int>();
33+
ids[i] = -1;
34+
}
35+
}
36+
37+
/// <summary>
38+
/// Adds a directed edge from u to v.
39+
/// </summary>
40+
public void AddEdge(int u, int v)
41+
{
42+
if (u < 0 || u >= graph.Length || v < 0 || v >= graph.Length)
43+
{
44+
throw new ArgumentOutOfRangeException(nameof(u), "Vertex indices must be within valid range.");
45+
}
46+
47+
graph[u].Add(v);
48+
}
49+
50+
/// <summary>
51+
/// Finds all strongly connected components.
52+
/// </summary>
53+
/// <returns>List of SCCs, where each SCC is a list of vertex indices.</returns>
54+
public List<List<int>> FindSCCs()
55+
{
56+
for (int i = 0; i < graph.Length; i++)
57+
{
58+
if (ids[i] == -1)
59+
{
60+
Dfs(i);
61+
}
62+
}
63+
64+
return sccs;
65+
}
66+
67+
/// <summary>
68+
/// Gets the number of strongly connected components.
69+
/// </summary>
70+
public int GetSccCount() => sccs.Count;
71+
72+
/// <summary>
73+
/// Checks if two vertices are in the same SCC.
74+
/// </summary>
75+
public bool InSameScc(int u, int v)
76+
{
77+
if (sccs.Count == 0)
78+
{
79+
FindSCCs();
80+
}
81+
82+
foreach (var scc in sccs)
83+
{
84+
if (scc.Contains(u) && scc.Contains(v))
85+
{
86+
return true;
87+
}
88+
}
89+
90+
return false;
91+
}
92+
93+
/// <summary>
94+
/// Gets the SCC containing the given vertex.
95+
/// </summary>
96+
public List<int>? GetScc(int vertex)
97+
{
98+
if (sccs.Count == 0)
99+
{
100+
FindSCCs();
101+
}
102+
103+
return sccs.FirstOrDefault(scc => scc.Contains(vertex));
104+
}
105+
106+
/// <summary>
107+
/// Builds the condensation graph (DAG of SCCs).
108+
/// </summary>
109+
public List<int>[] BuildCondensationGraph()
110+
{
111+
if (sccs.Count == 0)
112+
{
113+
FindSCCs();
114+
}
115+
116+
var sccIndex = new int[graph.Length];
117+
for (int i = 0; i < sccs.Count; i++)
118+
{
119+
foreach (var vertex in sccs[i])
120+
{
121+
sccIndex[vertex] = i;
122+
}
123+
}
124+
125+
var condensation = new List<int>[sccs.Count];
126+
for (int i = 0; i < sccs.Count; i++)
127+
{
128+
condensation[i] = new List<int>();
129+
}
130+
131+
var edges = new HashSet<(int, int)>();
132+
for (int u = 0; u < graph.Length; u++)
133+
{
134+
foreach (var v in graph[u])
135+
{
136+
int sccU = sccIndex[u];
137+
int sccV = sccIndex[v];
138+
139+
if (sccU != sccV && !edges.Contains((sccU, sccV)))
140+
{
141+
condensation[sccU].Add(sccV);
142+
edges.Add((sccU, sccV));
143+
}
144+
}
145+
}
146+
147+
return condensation;
148+
}
149+
150+
private void Dfs(int at)
151+
{
152+
stack.Push(at);
153+
onStack[at] = true;
154+
ids[at] = low[at] = id++;
155+
156+
foreach (var to in graph[at])
157+
{
158+
if (ids[to] == -1)
159+
{
160+
Dfs(to);
161+
}
162+
163+
if (onStack[to])
164+
{
165+
low[at] = Math.Min(low[at], low[to]);
166+
}
167+
}
168+
169+
if (ids[at] == low[at])
170+
{
171+
var scc = new List<int>();
172+
while (true)
173+
{
174+
int node = stack.Pop();
175+
onStack[node] = false;
176+
scc.Add(node);
177+
if (node == at)
178+
{
179+
break;
180+
}
181+
}
182+
183+
sccs.Add(scc);
184+
}
185+
}
186+
}

0 commit comments

Comments
 (0)