Skip to content

Commit

Permalink
Support SCC iteration via SCCSearch
Browse files Browse the repository at this point in the history
  • Loading branch information
hansonchar committed Jan 24, 2025
1 parent 47cf7e6 commit 5449893
Show file tree
Hide file tree
Showing 6 changed files with 243 additions and 11 deletions.
6 changes: 4 additions & 2 deletions learning-lua/algo/DFS.lua
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ local DFS = GraphSearch:class()
local E = {}

local function debug(...)
print(...)
-- print(...)
end

--- Push all unvisited outgoing edges to the stack.
Expand Down Expand Up @@ -99,11 +99,13 @@ local function _iterate(self)
if t then -- t is nil only during the first iteration when we are starting with the source node.
t[#t + 1] = count_unvisited -- append the number of unvisited outgoing edges.
t[#t + 1] = count_visited -- append the number of visited outgoing edges.
t[#t + 1] = beginning -- append the source vertex.
self._yield(t)
elseif push_count == 0 and from == beginning then -- the starting node is the only node with unvisited edges.
visited[from], self._visited_count = true, self._visited_count + 1
(unvisited_vertices or E)[from] = nil
self._yield {nil, nil, 0, from} -- yield it anyway so we don't loose a vertex during DFS
-- format: to, weight, depth, from, count_unvisited, count_visited, beginning
self._yield {nil, nil, 0, from, 0, 0, beginning} -- yield it anyway so we don't loose a vertex during DFS
end
end
t = (stack:pop() or E)
Expand Down
51 changes: 51 additions & 0 deletions learning-lua/algo/SCCSearch-tests.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
local Graph = require "algo.Graph"
local SCCSearch = require "algo.SCCSearch"
local E = {}

local function debug(...)
-- print(...)
end
---@param input (table) array of directed arcs in the format of, for example, "u-v".
---@return (table) a Graph object
local function load_edges(input)
local G = Graph:new()
for _, edge in ipairs(input) do
local from, to = edge:match('(%w-)%-(%w+).*')
G:add(from, to, 1)
end
return G
end

local function scc_test(G)
local sccs = {}
for scc, id, count in SCCSearch:new(G):iterate() do
debug(scc, id, count)
sccs[id] = sccs[id] or {}
sccs[id][scc] = true
end
assert(#sccs == 4)
for _, scc in ipairs {'6', '8', '10'} do
assert(sccs[1][scc])
end
local i, j
if next(sccs[2]) == '11' then
i, j = 2, 3
else
i, j = 3, 2
end
assert(sccs[i]['11'])
for _, scc in ipairs {'2', '4', '7', '9'} do
assert(sccs[j][scc])
end
for _, scc in ipairs {'1', '3', '5'} do
assert(sccs[4][scc])
end
end

-- Algoriths Illuminated Part 2 by Prof. Tim Roughgarden
print("Finding SCC using Graph of Figure 8.16 by Tim ...")
local G = load_edges {'1-3', '3-5', '5-1', '3-11', '5-9', '5-7', '11-6', '11-8', '8-6', '6-10', '10-8', '9-2', '9-4',
'2-4', '2-10', '4-7', '7-9'}

scc_test(G)
os.exit()
58 changes: 58 additions & 0 deletions learning-lua/algo/SCCSearch.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
local GraphSearch = require "algo.GraphSearch"
local Graph = require "algo.Graph"
local TopologicalSearch = require "algo.TopologicalSearch"
local DFS = require "algo.DFS"
local SCCSearch = GraphSearch:class()
local E = {}

local function debug(...)
-- print(...)
end

local function reversed(a)
local j = #a
for i = 1, #a do
if i >= j then
break
end
a[i], a[j] = a[j], a[i]
j = j - 1
end
return a
end

-- Uses the Kosaraju algorithm, but uses a topological search instead of DFS for the second pass.
-- Simpler code, but arguably less "traditional" as using DFS directly. (See SccDfsSearch.lua)
-- 1. Transpose the graph, then perform a topo search to get an array of vertexes in descending topo order.
-- 2. Transpose the graph again, then perform a topological iteration using the reversed array as the 'src_spec'.
-- A 'src_spec' is used to specify the order of source vertices to choose from whenenver
-- the search needs to restart from a different source vertex.
-- 3. We get a new SCC whenever the topological search retries from a different branch of the graph.
-- This also works, leveraging on a Topological search instead of DFS.
local function _iterate(self)
local src_spec = {}
local G = self.graph
G:transpose()
for v in TopologicalSearch:new(G):iterate() do
src_spec[#src_spec + 1] = v
end
G:transpose()
local scc_id, count = 1, 0
local src_spec = reversed(src_spec)
debug(table.concat(src_spec, ","))
local topo = TopologicalSearch:new(G, nil, src_spec)
for scc, is_first_scc in topo:iterate() do
count = count + 1
coroutine.yield(scc, scc_id, count)
if is_first_scc then
scc_id, count = scc_id + 1, 0
end
end
end

---@param G (table) graph
function SCCSearch:new(G)
return getmetatable(self):new(G, nil, _iterate)
end

return SCCSearch
51 changes: 51 additions & 0 deletions learning-lua/algo/SccDfsSearch-tests.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
local Graph = require "algo.Graph"
local SccDfsSearch = require "algo.SccDfsSearch"
local E = {}

local function debug(...)
-- print(...)
end
---@param input (table) array of directed arcs in the format of, for example, "u-v".
---@return (table) a Graph object
local function load_edges(input)
local G = Graph:new()
for _, edge in ipairs(input) do
local from, to = edge:match('(%w-)%-(%w+).*')
G:add(from, to, 1)
end
return G
end

local function scc_test(G)
local sccs = {}
for scc, id, count in SccDfsSearch:new(G):iterate() do
debug(scc, id, count)
sccs[id] = sccs[id] or {}
sccs[id][scc] = true
end
assert(#sccs == 4)
for _, scc in ipairs{'6', '8', '10'} do
assert(sccs[1][scc])
end
local i, j
if next(sccs[2]) == '11' then
i, j = 2, 3
else
i, j = 3, 2
end
assert(sccs[i]['11'])
for _, scc in ipairs{'2', '4', '7', '9'} do
assert(sccs[j][scc])
end
for _, scc in ipairs{'1', '3', '5'} do
assert(sccs[4][scc])
end
end

-- Algoriths Illuminated Part 2 by Prof. Tim Roughgarden
print("Testing SCC using Graph of Figure 8.16 by Tim ...")
local G = load_edges {'1-3', '3-5', '5-1', '3-11', '5-9', '5-7', '11-6', '11-8', '8-6', '6-10', '10-8', '9-2', '9-4',
'2-4', '2-10', '4-7', '7-9'}

scc_test(G)
os.exit()
68 changes: 68 additions & 0 deletions learning-lua/algo/SccDfsSearch.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
local GraphSearch = require "algo.GraphSearch"
local Graph = require "algo.Graph"
local TopologicalSearch = require "algo.TopologicalSearch"
local DFS = require "algo.DFS"
local SccDfsSearch = GraphSearch:class()
local E = {}

local function debug(...)
-- print(...)
end

local function reversed(a)
local j = #a
for i = 1, #a do
if i >= j then
break
end
a[i], a[j] = a[j], a[i]
j = j - 1
end
return a
end

-- Uses the Kosaraju algorithm.
-- 1. Transpose the graph, then perform a topo search to get an array of vertexes in descending topo order.
-- 2. Transpose the graph again, then perform a DFS iteration using the reversed array as the 'src_spec'.
-- A 'src_spec' is used to specify the order of source vertices to choose from whenenver
-- the search needs to restart from a different source vertex.
-- 3. We get a new SCC whenever the source vertex changes during the iteration.
local function _iterate(self)
local src_spec = {}
local G = self.graph
G:transpose()
for v in TopologicalSearch:new(G):iterate() do
src_spec[#src_spec + 1] = v
end
G:transpose()
local scc_id, count = 0, 0
local src_spec = reversed(src_spec)
debug(table.concat(src_spec, ","))
local dfs = DFS:new(G, nil, nil, src_spec)
local scc_src_vertex
for from, to, _, _, _, _, src_vertex in dfs:iterate() do
debug(string.format("from=%s to=%s, src_vertex=%s", from, to, src_vertex))
if scc_src_vertex == src_vertex then
if to then
count = count + 1
coroutine.yield(to, scc_id, count)
end
else
assert(from == src_vertex)
scc_id, count = scc_id + 1, 1
coroutine.yield(from, scc_id, count)
scc_src_vertex = src_vertex
if to then
count = count + 1
coroutine.yield(to, scc_id, count)
end
end
end
end

---@param G (table) graph
function SccDfsSearch:new(G)
return getmetatable(SccDfsSearch):new(G, nil, _iterate)
end

return SccDfsSearch
20 changes: 11 additions & 9 deletions learning-lua/algo/TopologicalSearch.lua
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ local Stack = require "algo.Stack"
local TopologicalSearch = GraphSearch:class()

local function debug(...)
-- print(...)
-- print(...)
end

--- Returns vertices in descending topological order.
Expand All @@ -18,29 +18,31 @@ local function _iterate(self, src)
stack:push(src)
end
for from, to in DFS:new(self.graph, src, nav_spec, src_spec):iterate() do
if not src then
if not src then -- this condition can be true at most once at the beginning of a topo search.
debug(string.format("Topo search starting from %s", from))
src = from
stack:push(src)
end
debug(string.format("%s-%s", from, to))
while stack:peek() ~= from do
while stack:peek() ~= from do -- enter the loop when the next node comes from a different branch.
local node = stack:pop()
if node then
coroutine.yield(node)
coroutine.yield(node, stack:empty())
else -- started from a different vertex
assert(not param_src) -- only possible if no src specified in the input parameter
debug(string.format("Topo search starting from %s", from))
src = from
stack:push(src)
debug(string.format("Topo search re-starting from %s", from))
stack:push(from)
end
end
if to then -- 'to' can be nil if 'from' is a lone starting vertex
stack:push(to)
end
end
while not stack:empty() do
coroutine.yield(stack:pop())
local empty = stack:empty()
while not empty do
local node = stack:pop()
empty = stack:empty()
coroutine.yield(node, empty)
end
end

Expand Down

0 comments on commit 5449893

Please sign in to comment.