From ade3ea4c526c939f41e5f34fdee589bafb976c84 Mon Sep 17 00:00:00 2001 From: Dunya Kirkali Date: Wed, 27 Dec 2023 17:58:58 +0100 Subject: [PATCH 1/4] Add common module --- lib/graph/common.ex | 9 +++++++++ lib/graph/undirected.ex | 2 ++ 2 files changed, 11 insertions(+) create mode 100644 lib/graph/common.ex create mode 100644 lib/graph/undirected.ex diff --git a/lib/graph/common.ex b/lib/graph/common.ex new file mode 100644 index 0000000..ee926b9 --- /dev/null +++ b/lib/graph/common.ex @@ -0,0 +1,9 @@ +defmodule Graph.Common do + def reachable(%Graph{type: :directed} = g, vs) when is_struct(g) and is_list(vs) do + Graph.Directed.reachable(g, vs) + end + + def reachable(%Graph{type: :undirected} = g, vs) when is_struct(g) and is_list(vs) do + Graph.Undirected.reachable(g, vs) + end +end diff --git a/lib/graph/undirected.ex b/lib/graph/undirected.ex new file mode 100644 index 0000000..e5f30e5 --- /dev/null +++ b/lib/graph/undirected.ex @@ -0,0 +1,2 @@ +defmodule Graph.Undirected do +end From e5e7614e2d8350f6478922c617b4490d28abf07c Mon Sep 17 00:00:00 2001 From: Dunya Kirkali Date: Wed, 27 Dec 2023 19:18:26 +0100 Subject: [PATCH 2/4] Extract reachable --- lib/graph.ex | 2 +- lib/graph/undirected.ex | 77 +++++++++++++++++++++++++++++++++++ test/graph/undirected_test.ex | 38 +++++++++++++++++ 3 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 test/graph/undirected_test.ex diff --git a/lib/graph.ex b/lib/graph.ex index 679ede0..8a96e92 100644 --- a/lib/graph.ex +++ b/lib/graph.ex @@ -1547,7 +1547,7 @@ defmodule Graph do [:d, :c, :b, :a] """ @spec reachable(t, [vertex]) :: [[vertex]] - defdelegate reachable(g, vs), to: Graph.Directed + defdelegate reachable(g, vs), to: Graph.Common @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 index e5f30e5..ffec194 100644 --- a/lib/graph/undirected.ex +++ b/lib/graph/undirected.ex @@ -1,2 +1,79 @@ defmodule Graph.Undirected do + @compile {:inline} + + 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 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/graph/undirected_test.ex b/test/graph/undirected_test.ex new file mode 100644 index 0000000..0706d94 --- /dev/null +++ b/test/graph/undirected_test.ex @@ -0,0 +1,38 @@ +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 +end From 41f6c0a3f7e08a2b2261dc8f3af34bb93289932e Mon Sep 17 00:00:00 2001 From: Dunya Kirkali Date: Wed, 27 Dec 2023 19:22:01 +0100 Subject: [PATCH 3/4] Boyscout --- lib/graph.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/graph.ex b/lib/graph.ex index 8a96e92..7600f4e 100644 --- a/lib/graph.ex +++ b/lib/graph.ex @@ -135,7 +135,7 @@ defmodule Graph do NOTE: Currently this function assumes graphs are directed graphs, but in the future it will support undirected graphs as well. - NOTE 2: To avoid to overwrite vertices with the same label, output is + NOTE 2: To avoid to overwrite vertices with the same label, output is generated using the internal numeric ID as vertex label. Original label is expressed as `id[label="