Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: start implementing Undirected module #58

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions lib/graph.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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`),
Expand All @@ -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`),
Expand Down
91 changes: 91 additions & 0 deletions lib/graph/undirected.ex
Original file line number Diff line number Diff line change
@@ -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
73 changes: 73 additions & 0 deletions test/undirected_graph_test.exs
Original file line number Diff line number Diff line change
@@ -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