Current section

Files

Jump to
yog_ex lib yog connectivity components.ex
Raw

lib/yog/connectivity/components.ex

defmodule Yog.Connectivity.Components do
@moduledoc """
Algorithms for finding (weakly) connected components in graphs.
"""
alias Yog.Model
@type component :: [Yog.node_id()]
@doc """
Finds Connected Components in an **undirected graph**.
A connected component is a maximal subgraph where every node is reachable
from every other node via undirected edges.
Time Complexity: O(V + E)
"""
@spec connected_components(Yog.graph()) :: [component()]
def connected_components(graph) do
all_nodes = Model.all_nodes(graph)
{_, components} =
Enum.reduce(all_nodes, {MapSet.new(), []}, fn node, {visited, components} ->
if MapSet.member?(visited, node) do
{visited, components}
else
{new_visited, component} = dfs_component(graph, node, visited, [])
{new_visited, [component | components]}
end
end)
components
end
defp dfs_component(graph, node, visited, component) do
visited = MapSet.put(visited, node)
neighbors = Model.successor_ids(graph, node)
Enum.reduce(neighbors, {visited, [node | component]}, fn neighbor, {acc_visited, acc_comp} ->
if MapSet.member?(acc_visited, neighbor) do
{acc_visited, acc_comp}
else
dfs_component(graph, neighbor, acc_visited, acc_comp)
end
end)
end
@doc """
Finds Weakly Connected Components in a **directed graph**.
A weakly connected component is a maximal subgraph where, if you ignore
edge directions, all nodes are reachable from each other.
Time Complexity: O(V + E)
"""
@spec weakly_connected_components(Yog.graph()) :: [component()]
def weakly_connected_components(graph) do
all_nodes = Model.all_nodes(graph)
{_, components} =
Enum.reduce(all_nodes, {MapSet.new(), []}, fn node, {visited, components} ->
if MapSet.member?(visited, node) do
{visited, components}
else
{new_visited, component} = dfs_weak_component(graph, node, visited, [])
{new_visited, [component | components]}
end
end)
components
end
defp dfs_weak_component(graph, node, visited, component) do
visited = MapSet.put(visited, node)
# Get all neighbors (both successors and predecessors)
successors = Model.successor_ids(graph, node)
predecessors = Model.predecessors(graph, node) |> Enum.map(fn {id, _} -> id end)
all_neighbors = Enum.uniq(successors ++ predecessors)
Enum.reduce(all_neighbors, {visited, [node | component]}, fn neighbor,
{acc_visited, acc_comp} ->
if MapSet.member?(acc_visited, neighbor) do
{acc_visited, acc_comp}
else
dfs_weak_component(graph, neighbor, acc_visited, acc_comp)
end
end)
end
end