Skip to content

Commit fd03d46

Browse files
committed
Add strongly connected components graph algorithm
1 parent 3542285 commit fd03d46

File tree

2 files changed

+99
-0
lines changed

2 files changed

+99
-0
lines changed

.spec/data_structures/graph_spec.lua

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,17 @@ describe("Graph", function()
126126
local g = graph.new({ a = { b = 42 }, b = {} })
127127
assert.same(g, g:copy())
128128
end)
129+
it("transposed", function()
130+
local g = graph.new({ a = { b = 42 }, b = {}, c = { a = 1, b = 2 } })
131+
assert.same(
132+
graph.new({
133+
a = { c = 1 },
134+
b = { a = 42, c = 2 },
135+
c = {},
136+
}),
137+
g:transposed()
138+
)
139+
end)
129140
local function traversal_common(fname)
130141
local function visits_all_nodes(g)
131142
local expected_nodes = {}
@@ -216,6 +227,37 @@ describe("Graph", function()
216227
end
217228
end)
218229
end)
230+
describe("strongly connected components", function()
231+
it("works on a simple example", function()
232+
local g = graph.new({
233+
[1] = { [2] = true },
234+
[2] = { [1] = true, [3] = true },
235+
[3] = { [4] = true },
236+
[4] = { [3] = true },
237+
})
238+
assert.same({
239+
{ [1] = true, [2] = true },
240+
{ [3] = true, [4] = true },
241+
}, g:strongly_connected_components())
242+
end)
243+
it("sorts result topologically", function()
244+
for _ = 1, 10 do
245+
local g = random_directed_acyclic_graph()
246+
local rank = {}
247+
local i = 0
248+
for _, component in ipairs(g:strongly_connected_components()) do
249+
local node = assert(next(component))
250+
assert.same({ [node] = true }, component)
251+
assert.equal(nil, next(component, node))
252+
i = i + 1
253+
rank[node] = i
254+
end
255+
for from, to in g:edges() do
256+
assert(rank[from] < rank[to])
257+
end
258+
end
259+
end)
260+
end)
219261
local function test_sssp_limited(name, negative_weights)
220262
it("selected graph", negative_weights and function()
221263
-- This is an example where the invariant of Dijkstra's algorithm would be broken

src/data_structures/graph.lua

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,18 @@ function graph:edges()
109109
end)
110110
end
111111

112+
--> graph with reversed edge direction
113+
function graph:transposed()
114+
local transposed = graph.new()
115+
for node in self:nodes() do
116+
transposed:add_node(node)
117+
end
118+
for from, to, weight in self:edges() do
119+
transposed:set_weight(to, from, weight)
120+
end
121+
return transposed
122+
end
123+
112124
-- Breadth-first traversal. Can be used to solve shortest path problems if all edges have the same weight.
113125
function graph:nodes_breadth_first(
114126
root -- optional root node to start the traversal from
@@ -248,6 +260,51 @@ function graph:nodes_topological_order()
248260
end
249261
end
250262

263+
-- Kosaraju's algorithm
264+
--> list of strongly connected components (sets of nodes), topologically sorted
265+
function graph:strongly_connected_components()
266+
local nodes_depth_first = {}
267+
do -- "postorder", depth-first traversal
268+
local seen = {}
269+
local function visit(node)
270+
if seen[node] then
271+
return
272+
end
273+
seen[node] = true
274+
for neighbor in self:neighbors(node) do
275+
visit(neighbor)
276+
end
277+
table.insert(nodes_depth_first, node)
278+
end
279+
for node in self:nodes() do
280+
visit(node)
281+
end
282+
end
283+
local transposed = self:transposed()
284+
local seen = {}
285+
local connected_components = {}
286+
for i = #nodes_depth_first, 1, -1 do
287+
local root = nodes_depth_first[i]
288+
if not seen[root] then
289+
seen[root] = true
290+
local component = {}
291+
local to_visit = { root }
292+
repeat
293+
local node = table.remove(to_visit)
294+
component[node] = true
295+
for neighbor in transposed:neighbors(node) do
296+
if not seen[neighbor] then
297+
seen[neighbor] = true
298+
table.insert(to_visit, neighbor)
299+
end
300+
end
301+
until to_visit[1] == nil
302+
table.insert(connected_components, component)
303+
end
304+
end
305+
return connected_components
306+
end
307+
251308
-- Single source shortest paths using Dijkstra.
252309
-- Requires positive weights.
253310
function graph:sssp_dijkstra(source)

0 commit comments

Comments
 (0)