Current section
Files
Jump to
Current section
Files
src/yog@components.erl
-module(yog@components).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/yog/components.gleam").
-export([strongly_connected_components/1, kosaraju/1]).
-export_type([tarjan_state/0]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
-type tarjan_state() :: {tarjan_state,
integer(),
list(integer()),
gleam@dict:dict(integer(), boolean()),
gleam@dict:dict(integer(), integer()),
gleam@dict:dict(integer(), integer()),
list(list(integer()))}.
-file("src/yog/components.gleam", 105).
-spec pop_stack_until(integer(), tarjan_state(), list(integer())) -> tarjan_state().
pop_stack_until(U, State, Component) ->
case erlang:element(3, State) of
[] ->
State;
[Head | Tail] ->
New_component = [Head | Component],
New_on_stack = gleam@dict:insert(
erlang:element(4, State),
Head,
false
),
Next_state = {tarjan_state,
erlang:element(2, State),
Tail,
New_on_stack,
erlang:element(5, State),
erlang:element(6, State),
erlang:element(7, State)},
case Head =:= U of
true ->
{tarjan_state,
erlang:element(2, Next_state),
erlang:element(3, Next_state),
erlang:element(4, Next_state),
erlang:element(5, Next_state),
erlang:element(6, Next_state),
[New_component | erlang:element(7, State)]};
false ->
pop_stack_until(U, Next_state, New_component)
end
end.
-file("src/yog/components.gleam", 46).
-spec strong_connect(yog@model:graph(any(), any()), integer(), tarjan_state()) -> tarjan_state().
strong_connect(Graph, U, State) ->
State@1 = {tarjan_state,
erlang:element(2, State) + 1,
[U | erlang:element(3, State)],
gleam@dict:insert(erlang:element(4, State), U, true),
gleam@dict:insert(erlang:element(5, State), U, erlang:element(2, State)),
gleam@dict:insert(erlang:element(6, State), U, erlang:element(2, State)),
erlang:element(7, State)},
Successors = yog@model:successor_ids(Graph, U),
State@2 = gleam@list:fold(
Successors,
State@1,
fun(St, V) -> case gleam@dict:has_key(erlang:element(5, St), V) of
false ->
St@1 = strong_connect(Graph, V, St),
U_low = begin
_pipe = gleam_stdlib:map_get(erlang:element(6, St@1), U),
gleam@result:unwrap(_pipe, 0)
end,
V_low = begin
_pipe@1 = gleam_stdlib:map_get(
erlang:element(6, St@1),
V
),
gleam@result:unwrap(_pipe@1, 0)
end,
{tarjan_state,
erlang:element(2, St@1),
erlang:element(3, St@1),
erlang:element(4, St@1),
erlang:element(5, St@1),
gleam@dict:insert(
erlang:element(6, St@1),
U,
gleam@int:min(U_low, V_low)
),
erlang:element(7, St@1)};
true ->
case begin
_pipe@2 = gleam_stdlib:map_get(erlang:element(4, St), V),
gleam@result:unwrap(_pipe@2, false)
end of
true ->
U_low@1 = begin
_pipe@3 = gleam_stdlib:map_get(
erlang:element(6, St),
U
),
gleam@result:unwrap(_pipe@3, 0)
end,
V_index = begin
_pipe@4 = gleam_stdlib:map_get(
erlang:element(5, St),
V
),
gleam@result:unwrap(_pipe@4, 0)
end,
{tarjan_state,
erlang:element(2, St),
erlang:element(3, St),
erlang:element(4, St),
erlang:element(5, St),
gleam@dict:insert(
erlang:element(6, St),
U,
gleam@int:min(U_low@1, V_index)
),
erlang:element(7, St)};
false ->
St
end
end end
),
U_index = begin
_pipe@5 = gleam_stdlib:map_get(erlang:element(5, State@2), U),
gleam@result:unwrap(_pipe@5, 0)
end,
U_low@2 = begin
_pipe@6 = gleam_stdlib:map_get(erlang:element(6, State@2), U),
gleam@result:unwrap(_pipe@6, 0)
end,
case U_low@2 =:= U_index of
true ->
pop_stack_until(U, State@2, []);
false ->
State@2
end.
-file("src/yog/components.gleam", 22).
?DOC(
" Finds Strongly Connected Components (SCC) using Tarjan's Algorithm.\n"
" Returns a list of components, where each component is a list of NodeIds.\n"
).
-spec strongly_connected_components(yog@model:graph(any(), any())) -> list(list(integer())).
strongly_connected_components(Graph) ->
Nodes = yog@model:all_nodes(Graph),
Initial_state = {tarjan_state,
0,
[],
maps:new(),
maps:new(),
maps:new(),
[]},
Final_state = gleam@list:fold(
Nodes,
Initial_state,
fun(State, Node) ->
case gleam@dict:has_key(erlang:element(5, State), Node) of
true ->
State;
false ->
strong_connect(Graph, Node, State)
end
end
),
erlang:element(7, Final_state).
-file("src/yog/components.gleam", 196).
-spec first_dfs(
yog@model:graph(any(), any()),
integer(),
gleam@set:set(integer()),
list(integer())
) -> {list(integer()), gleam@set:set(integer())}.
first_dfs(Graph, Node, Visited, Stack) ->
case gleam@set:contains(Visited, Node) of
true ->
{Stack, Visited};
false ->
New_visited = gleam@set:insert(Visited, Node),
Successors = yog@model:successor_ids(Graph, Node),
{New_stack, Final_visited} = gleam@list:fold(
Successors,
{Stack, New_visited},
fun(Acc, Succ) ->
{S, V} = Acc,
first_dfs(Graph, Succ, V, S)
end
),
{[Node | New_stack], Final_visited}
end.
-file("src/yog/components.gleam", 222).
-spec second_dfs(
yog@model:graph(any(), any()),
integer(),
gleam@set:set(integer()),
list(integer())
) -> {list(integer()), gleam@set:set(integer())}.
second_dfs(Transposed, Node, Visited, Component) ->
case gleam@set:contains(Visited, Node) of
true ->
{Component, Visited};
false ->
New_visited = gleam@set:insert(Visited, Node),
New_component = [Node | Component],
Successors = yog@model:successor_ids(Transposed, Node),
gleam@list:fold(
Successors,
{New_component, New_visited},
fun(Acc, Succ) ->
{Comp, Vis} = Acc,
second_dfs(Transposed, Succ, Vis, Comp)
end
)
end.
-file("src/yog/components.gleam", 165).
?DOC(
" Finds Strongly Connected Components (SCC) using Kosaraju's Algorithm.\n"
"\n"
" Returns a list of components, where each component is a list of NodeIds.\n"
" Kosaraju's algorithm uses two DFS passes and graph transposition:\n"
"\n"
" 1. First DFS: Compute finishing times (nodes added to stack when DFS completes)\n"
" 2. Transpose the graph (reverse all edges) - O(1) operation!\n"
" 3. Second DFS: Process nodes in reverse finishing time order on transposed graph\n"
"\n"
" **Time Complexity:** O(V + E) where V is vertices and E is edges\n"
" **Space Complexity:** O(V) for the visited set and finish stack\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" let graph =\n"
" model.new(Directed)\n"
" |> model.add_node(1, \"A\")\n"
" |> model.add_node(2, \"B\")\n"
" |> model.add_node(3, \"C\")\n"
" |> model.add_edge(from: 1, to: 2, with: 1)\n"
" |> model.add_edge(from: 2, to: 3, with: 1)\n"
" |> model.add_edge(from: 3, to: 1, with: 1)\n"
"\n"
" let sccs = components.kosaraju(graph)\n"
" // => [[1, 2, 3]] // All nodes form one SCC (cycle)\n"
" ```\n"
"\n"
" ## Comparison with Tarjan's Algorithm\n"
"\n"
" - **Kosaraju:** Two DFS passes, requires graph transposition, simpler to understand\n"
" - **Tarjan:** Single DFS pass, no transposition needed, uses low-link values\n"
"\n"
" Both have the same O(V + E) time complexity, but Kosaraju may be preferred when:\n"
" - The graph is already stored in a format supporting fast transposition\n"
" - Simplicity and clarity are prioritized over single-pass execution\n"
).
-spec kosaraju(yog@model:graph(any(), any())) -> list(list(integer())).
kosaraju(Graph) ->
Nodes = yog@model:all_nodes(Graph),
{Finish_stack, _} = gleam@list:fold(
Nodes,
{[], gleam@set:new()},
fun(Acc, Node) ->
{Stack, Visited} = Acc,
first_dfs(Graph, Node, Visited, Stack)
end
),
Transposed = yog@transform:transpose(Graph),
{Components@1, _} = gleam@list:fold(
Finish_stack,
{[], gleam@set:new()},
fun(Acc@1, Node@1) ->
{Components, Visited@1} = Acc@1,
case gleam@set:contains(Visited@1, Node@1) of
true ->
Acc@1;
false ->
{Component, New_visited} = second_dfs(
Transposed,
Node@1,
Visited@1,
[]
),
{[Component | Components], New_visited}
end
end
),
Components@1.