Current section
Files
Jump to
Current section
Files
lib/yog/functional/analysis.ex
defmodule Yog.Functional.Analysis do
@moduledoc """
Structural analysis for inductive graphs — components, bridges, and articulation points.
This module analyzes connectivity and vulnerability in graphs using the inductive
`match/2` operation for component extraction and Tarjan's DFS for bridge/cut-vertex
detection.
## Available Analyses
| Analysis | Function | Description |
|----------|----------|-------------|
| Connected Components | `connected_components/1` | Find all connected components |
| Bridges & Articulation Points | `analyze_connectivity/1` | Single-pass Tarjan DFS |
## Key Concepts
- **Bridge** (cut-edge): An edge whose removal disconnects the graph
- **Articulation Point** (cut-vertex): A node whose removal disconnects the graph
- Components are extracted inductively via `match/2`, naturally preventing
revisits without an explicit visited set
## References
- [Wikipedia: Bridge (Graph Theory)](https://en.wikipedia.org/wiki/Bridge_(graph_theory))
- [Wikipedia: Biconnected Component](https://en.wikipedia.org/wiki/Biconnected_component)
"""
alias Yog.Functional.Model
@type bridge :: {Model.node_id(), Model.node_id()}
@doc """
Finds all connected components in an undirected graph.
Returns a list of lists of node IDs.
"""
@spec connected_components(Model.t()) :: [[Model.node_id()]]
def connected_components(graph) do
do_find_components(graph, [])
end
defp do_find_components(graph, acc) do
if Model.empty?(graph) do
Enum.reverse(acc)
else
[start_id | _] = Model.node_ids(graph)
{component, remaining_graph} = extract_component(graph, [start_id], [])
do_find_components(remaining_graph, [component | acc])
end
end
@doc """
Identifies bridges (cut-edges) and articulation points (cut-vertices)
in an undirected graph using a single-pass DFS.
"""
@spec analyze_connectivity(Model.t()) ::
%{bridges: [bridge()], points: [Model.node_id()]}
def analyze_connectivity(graph) do
initial_state = %{
tin: %{},
low: %{},
timer: 0,
bridges: [],
points: MapSet.new(),
visited: MapSet.new()
}
final_state =
Enum.reduce(Model.node_ids(graph), initial_state, fn id, acc ->
if MapSet.member?(acc.visited, id) do
acc
else
tarjan_dfs(graph, id, nil, acc) |> elem(0)
end
end)
%{bridges: final_state.bridges, points: MapSet.to_list(final_state.points)}
end
defp tarjan_dfs(graph, v, parent, state) do
tin = Map.put(state.tin, v, state.timer)
low = Map.put(state.low, v, state.timer)
visited = MapSet.put(state.visited, v)
timer = state.timer + 1
base_state = %{state | tin: tin, low: low, visited: visited, timer: timer}
{:ok, ctx} = Model.get_node(graph, v)
neighbors = Map.keys(ctx.out_edges)
{reduce_state, children_count} =
Enum.reduce(neighbors, {base_state, 0}, fn to, {acc_state, children} ->
process_neighbor(graph, v, to, parent, acc_state, children)
end)
final_state =
if parent == nil and children_count > 1 do
%{reduce_state | points: MapSet.put(reduce_state.points, v)}
else
reduce_state
end
{final_state, children_count}
end
defp process_neighbor(graph, v, to, parent, acc_state, children) do
cond do
to == parent ->
{acc_state, children}
MapSet.member?(acc_state.visited, to) ->
new_low = min(acc_state.low[v], acc_state.tin[to])
{%{acc_state | low: Map.put(acc_state.low, v, new_low)}, children}
true ->
{post_dfs_state, _} = tarjan_dfs(graph, to, v, acc_state)
new_v_low = min(post_dfs_state.low[v], post_dfs_state.low[to])
new_bridges =
if post_dfs_state.low[to] > post_dfs_state.tin[v] do
[{min(v, to), max(v, to)} | post_dfs_state.bridges]
else
post_dfs_state.bridges
end
new_points =
if parent != nil and post_dfs_state.low[to] >= post_dfs_state.tin[v] do
MapSet.put(post_dfs_state.points, v)
else
post_dfs_state.points
end
{%{
post_dfs_state
| low: Map.put(post_dfs_state.low, v, new_v_low),
bridges: new_bridges,
points: new_points
}, children + 1}
end
end
defp extract_component(graph, [], acc), do: {acc, graph}
defp extract_component(graph, [id | stack], acc) do
case Model.match(graph, id) do
{:error, :not_found} ->
extract_component(graph, stack, acc)
{:ok, ctx, remaining} ->
neighbors = Map.keys(ctx.out_edges)
extract_component(remaining, neighbors ++ stack, [id | acc])
end
end
end