defmodule Yog.Connectivity.Analysis do @moduledoc """ Algorithms for analyzing graph connectivity (bridges, articulation points). """ alias Yog.Model @type bridge :: {Yog.node_id(), Yog.node_id()} @type connectivity_results :: %{ bridges: [bridge()], articulation_points: [Yog.node_id()] } @doc """ Analyzes an **undirected graph** to find all bridges and articulation points. """ @spec analyze(keyword() | Yog.graph()) :: connectivity_results() def analyze(options_or_graph) do graph = if is_list(options_or_graph) do Keyword.fetch!(options_or_graph, :in) else options_or_graph end do_analyze(graph) end defp do_analyze(graph) do all_nodes = Model.all_nodes(graph) initial_state = %{ disc: %{}, low: %{}, parent: %{}, time: 0, bridges: [], articulation_points: MapSet.new() } final_state = Enum.reduce(all_nodes, initial_state, fn node, state -> if Map.has_key?(state.disc, node) do state else analyze_dfs(graph, node, state) end end) %{ bridges: Enum.sort(final_state.bridges), articulation_points: Enum.sort(MapSet.to_list(final_state.articulation_points)) } end defp analyze_dfs(graph, node, state) do state = Map.update!(state, :disc, &Map.put(&1, node, state.time)) |> Map.update!(:low, &Map.put(&1, node, state.time)) |> Map.update!(:time, &(&1 + 1)) neighbors = Model.successor_ids(graph, node) children_count = init_counter(0) state_after_neighbors = Enum.reduce(neighbors, state, fn neighbor, acc_state -> case Map.fetch(acc_state.disc, neighbor) do :error -> increment_counter(children_count) new_state = Map.update!(acc_state, :parent, &Map.put(&1, neighbor, node)) |> then(&analyze_dfs(graph, neighbor, &1)) new_low = min(Map.fetch!(new_state.low, node), Map.fetch!(new_state.low, neighbor)) new_state = Map.update!(new_state, :low, &Map.put(&1, node, new_low)) parent = Map.get(new_state.parent, node) new_state = if parent != nil and Map.fetch!(new_state.low, neighbor) >= Map.fetch!(new_state.disc, node) do Map.update!(new_state, :articulation_points, &MapSet.put(&1, node)) else new_state end new_state = if Map.fetch!(new_state.low, neighbor) > Map.fetch!(new_state.disc, node) do bridge = if node < neighbor, do: {node, neighbor}, else: {neighbor, node} Map.update!(new_state, :bridges, &[bridge | &1]) else new_state end new_state {:ok, _} -> if Map.get(acc_state.parent, node) != neighbor do new_low = min(Map.fetch!(acc_state.low, node), Map.fetch!(acc_state.disc, neighbor)) Map.update!(acc_state, :low, &Map.put(&1, node, new_low)) else acc_state end end end) if Map.get(state_after_neighbors.parent, node) == nil and get_counter(children_count) > 1 do Map.update!(state_after_neighbors, :articulation_points, &MapSet.put(&1, node)) else state_after_neighbors end end defp init_counter(initial) do key = make_ref() Process.put(key, initial) key end defp get_counter(key), do: Process.get(key) defp increment_counter(key) do current = Process.get(key) Process.put(key, current + 1) end end