diff --git a/lib/graph.ex b/lib/graph.ex index 78d1859..fae2255 100644 --- a/lib/graph.ex +++ b/lib/graph.ex @@ -1534,7 +1534,8 @@ defmodule Graph do [:d, :c, :b, :a] """ @spec reachable(t, [vertex]) :: [[vertex]] - defdelegate reachable(g, vs), to: Graph.Directed + def reachable(%Graph{type: :undirected} = g, vs), do: Graph.Undirected.reachable(g, vs) + def reachable(%Graph{} = g, vs), do: Graph.Directed.reachable(g, vs) @doc """ Returns an unsorted list of vertices from the graph, such that for each vertex in the list (call it `v`), @@ -1550,7 +1551,10 @@ defmodule Graph do [:d, :c, :b] """ @spec reachable_neighbors(t, [vertex]) :: [[vertex]] - defdelegate reachable_neighbors(g, vs), to: Graph.Directed + def reachable_neighbors(%Graph{type: :undirected} = g, vs), + do: Graph.Undirected.reachable_neighbors(g, vs) + + def reachable_neighbors(%Graph{} = g, vs), do: Graph.Directed.reachable_neighbors(g, vs) @doc """ Returns an unsorted list of vertices from the graph, such that for each vertex in the list (call it `v`), diff --git a/lib/graph/undirected.ex b/lib/graph/undirected.ex new file mode 100644 index 0000000..8da54e2 --- /dev/null +++ b/lib/graph/undirected.ex @@ -0,0 +1,91 @@ +defmodule Graph.Undirected do + @moduledoc false + @compile {:inline, [neighbors: 2, neighbors: 3]} + + def reachable(%Graph{vertices: vertices, vertex_identifier: vertex_identifier} = g, vs) + when is_list(vs) do + vs = Enum.map(vs, vertex_identifier) + for id <- :lists.append(forest(g, &neighbors/3, vs, :first)), do: Map.get(vertices, id) + end + + def reachable_neighbors( + %Graph{vertices: vertices, vertex_identifier: vertex_identifier} = g, + vs + ) + when is_list(vs) do + vs = Enum.map(vs, vertex_identifier) + + for id <- :lists.append(forest(g, &neighbors/3, vs, :not_first)), + do: Map.get(vertices, id) + end + + def neighbors(%Graph{} = g, v, []) do + neighbors(g, v) + end + + def neighbors(%Graph{out_edges: oe, in_edges: ie}, v, vs) do + case {Map.get(ie, v), Map.get(oe, v)} do + {nil, nil} -> + vs + + {v_in, nil} -> + MapSet.to_list(v_in) ++ vs + + {nil, v_out} -> + MapSet.to_list(v_out) ++ vs + + {v_in, v_out} -> + s = MapSet.union(v_in, v_out) + MapSet.to_list(s) ++ vs + end + end + + def neighbors(%Graph{out_edges: oe, in_edges: ie}, v) do + v_in = Map.get(ie, v, MapSet.new()) + v_out = Map.get(oe, v, MapSet.new()) + + MapSet.union(v_in, v_out) + |> MapSet.to_list() + end + + defp forest(%Graph{vertices: vs} = g, fun) do + forest(g, fun, Map.keys(vs)) + end + + defp forest(g, fun, vs) do + forest(g, fun, vs, :first) + end + + defp forest(g, fun, vs, handle_first) do + {_, acc} = + List.foldl(vs, {MapSet.new(), []}, fn v, {visited, acc} -> + pretraverse(handle_first, v, fun, g, visited, acc) + end) + + acc + end + + defp pretraverse(:first, v, fun, g, visited, acc) do + ptraverse([v], fun, g, visited, [], acc) + end + + defp pretraverse(:not_first, v, fun, g, visited, acc) do + if MapSet.member?(visited, v) do + {visited, acc} + else + ptraverse(fun.(g, v, []), fun, g, visited, [], acc) + end + end + + defp ptraverse([v | vs], fun, g, visited, results, acc) do + if MapSet.member?(visited, v) do + ptraverse(vs, fun, g, visited, results, acc) + else + visited = MapSet.put(visited, v) + ptraverse(fun.(g, v, vs), fun, g, visited, [v | results], acc) + end + end + + defp ptraverse([], _fun, _g, visited, [], acc), do: {visited, acc} + defp ptraverse([], _fun, _g, visited, results, acc), do: {visited, [results | acc]} +end diff --git a/test/undirected_graph_test.exs b/test/undirected_graph_test.exs new file mode 100644 index 0000000..1a786e5 --- /dev/null +++ b/test/undirected_graph_test.exs @@ -0,0 +1,73 @@ +defmodule Graph.UndirectedTest do + use ExUnit.Case, async: true + + describe "Graph.reachable/2" do + test "reachable" do + g = + Graph.new(type: :undirected) + |> Graph.add_edges([{:a, :b}, {:b, :c}]) + + assert [:a, :b, :c] = Graph.reachable(g, [:c]) + assert [:c, :b, :a] = Graph.reachable(g, [:a]) + end + + test "parts reachable" do + g = + Graph.new(type: :undirected) + |> Graph.add_edges([{:a, :b}, {:b, :c}, {:d, :e}]) + + assert [:d, :e] = Graph.reachable(g, [:e]) + assert [:c, :a, :b] = Graph.reachable(g, [:b]) + end + + test "nothing reachable" do + g = + Graph.new(type: :undirected) + |> Graph.add_edges([{:a, :b}, {:b, :d}]) + |> Graph.add_vertex(:c) + + assert [:c] = Graph.reachable(g, [:c]) + end + + test "unknown vertex" do + g = Graph.new(type: :undirected) + + assert [nil] = Graph.reachable(g, [:a]) + end + end + + describe "Graph.reachable_neighbours/2" do + test "reachable" do + g = + Graph.new(type: :undirected) + |> Graph.add_edges([{:a, :b}, {:b, :c}]) + + assert [:a, :b] = Graph.reachable_neighbors(g, [:c]) + end + + @tag :only + test "parts reachable" do + g = + Graph.new(type: :undirected) + |> Graph.add_edges([{:a, :b}, {:b, :c}, {:d, :e}, {:e, :f}]) + + assert [:d, :e] = Graph.reachable_neighbors(g, [:f]) + assert [] = Graph.reachable_neighbors(g, [:b]) + end + + test "nothing reachable" do + g = + Graph.new(type: :undirected) + |> Graph.add_edges([{:a, :b}, {:b, :d}]) + |> Graph.add_vertex(:c) + + assert [] = Graph.reachable_neighbors(g, [:c]) + end + + test "unknown vertex" do + g = Graph.new(type: :undirected) + + assert [] = Graph.reachable_neighbors(g, [:a]) + end + end +end