Skip to content

Commit 33e264e

Browse files
committed
Merge branch 'pr/31'
* pr/31: Fix bug in Graph.Reducers.Bfs inbound edge detection Use ID as vertex label when generating DOT output Add Bellman ford alogorythm
2 parents b06e431 + c7a3f2f commit 33e264e

7 files changed

Lines changed: 138 additions & 16 deletions

File tree

lib/graph.ex

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ defmodule Graph do
4545
@type edge_key :: {vertex_id, vertex_id}
4646
@type edge_value :: %{label => edge_weight}
4747
@type graph_type :: :directed | :undirected
48+
@type vertices :: %{vertex_id => vertex}
4849
@type t :: %__MODULE__{
4950
in_edges: %{vertex_id => MapSet.t()},
5051
out_edges: %{vertex_id => MapSet.t()},
@@ -320,6 +321,24 @@ defmodule Graph do
320321
@spec dijkstra(t, vertex, vertex) :: [vertex]
321322
defdelegate dijkstra(g, a, b), to: Graph.Pathfinding
322323

324+
@doc """
325+
## Example
326+
327+
iex> g = Graph.new |> Graph.add_edges([
328+
...> {:b, :c, weight: -2}, {:a, :b, weight: 1},
329+
...> {:c, :d, weight: 3}, {:b, :d, weight: 4}])
330+
...> Graph.bellman_ford(g, :a)
331+
%{97 => 0, 98 => 1, 99 => -1, 100 => 2}
332+
333+
iex> g = Graph.new |> Graph.add_edges([
334+
...> {:b, :c, weight: -2}, {:a, :b, weight: -1},
335+
...> {:c, :d, weight: -3}, {:d, :a, weight: -5}])
336+
...> Graph.bellman_ford(g, :a)
337+
nil
338+
"""
339+
@spec bellman_ford(t, vertex) :: [vertex]
340+
defdelegate bellman_ford(g, a), to: Graph.Pathfinding
341+
323342
@doc """
324343
Gets the shortest path between `a` and `b`.
325344

lib/graph/pathfinding.ex

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ defmodule Graph.Pathfinding do
66

77
@type heuristic_fun :: (Graph.vertex() -> integer)
88

9+
@spec bellman_ford(Graph.t, Graph.vertex) :: [Graph.vertex]
10+
def bellman_ford(g, a), do: Pathfindings.BellmanFord.call(g, a)
11+
912
@doc """
1013
Finds the shortest path between `a` and `b` as a list of vertices.
1114
Returns `nil` if no path can be found.
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
defmodule Pathfindings.BellmanFord do
2+
@moduledoc """
3+
The Bellman–Ford algorithm is an algorithm that computes shortest paths from a single
4+
source vertex to all of the other vertices in a weighted digraph.
5+
It is capable of handling graphs in which some of the edge weights are negative numbers
6+
7+
Time complexity: O(VLogV)
8+
"""
9+
import Graph.Utils, only: [vertex_id: 1]
10+
11+
@type distance :: %{Graph.vertex_id => integer}
12+
13+
@doc """
14+
Returns nil when graph has negative cycle.
15+
"""
16+
@spec call(Graph.t, Graph.vertex) :: [Graph.vertex] | nil
17+
def call(%Graph{vertices: vs, edges: meta}, a) do
18+
distances = a |> vertex_id |> init_distances(vs)
19+
20+
distances = vs |> Enum.reduce(distances, fn (_vertex, distances) ->
21+
meta |> Enum.reduce(distances, &update_distance(&1, &2))
22+
end)
23+
24+
if has_negative_cycle?(distances, meta) do
25+
nil
26+
else
27+
distances
28+
end
29+
end
30+
31+
@spec init_distances(Graph.vertex, Graph.vertices) :: distance
32+
defp init_distances(vertex_id, vertices) do
33+
Enum.reduce(vertices, Map.new, fn {id, _vertex}, dist ->
34+
Map.put(dist, id, init_distance_value(id, vertex_id))
35+
end)
36+
end
37+
38+
defp init_distance_value(vertex_id, id) when vertex_id == id, do: 0
39+
defp init_distance_value(_, _), do: :infinity
40+
41+
@spec update_distance(term, distance) :: distance
42+
defp update_distance({{u, v}, _} = edge, distances) do
43+
weight = edge_weight(edge)
44+
45+
if distances[u] != :infinity and distances[u] + weight < distances[v] do
46+
Map.replace!(distances, v, distances[u] + weight)
47+
else
48+
distances
49+
end
50+
end
51+
52+
@spec edge_weight(term) :: float
53+
defp edge_weight({_, edge_value}), do: edge_value |> Map.values |> List.first
54+
55+
defp has_negative_cycle?(distances, meta) do
56+
meta |> Enum.reduce(false, fn({{u, v}, _} = edge, has_cycle) ->
57+
weight = edge_weight(edge)
58+
59+
has_cycle or distances[u] != :infinity and distances[u] + weight < distances[v]
60+
end)
61+
end
62+
end

lib/graph/reducers/bfs.ex

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,18 +50,25 @@ defmodule Graph.Reducers.Bfs do
5050
...> #{__MODULE__}.reduce(g, [], fn 4, acc -> {:halt, acc}; v, acc -> {:next, [v|acc]} end)
5151
[3, 1]
5252
"""
53-
def reduce(%Graph{vertices: vs, in_edges: ie} = g, acc, fun) when is_function(fun, 2) do
53+
def reduce(%Graph{vertices: vs} = g, acc, fun) when is_function(fun, 2) do
5454
vs
5555
# Start with a cost of zero
5656
|> Stream.map(fn {id, _} -> {id, 0} end)
5757
# Only populate the initial queue with those vertices which have no inbound edges
58-
|> Stream.filter(fn {id, _cost} -> is_nil(Map.get(ie, id)) end)
58+
|> Stream.reject(fn {id, _cost} -> inbound_edges?(g, id) end)
5959
|> Enum.reduce(PriorityQueue.new(), fn {id, cost}, q ->
6060
PriorityQueue.push(q, id, cost)
6161
end)
6262
|> traverse(g, MapSet.new(), fun, acc)
6363
end
6464

65+
defp inbound_edges?(%Graph{in_edges: ie}, v_id) do
66+
case Map.get(ie, v_id) do
67+
nil -> false
68+
edges -> MapSet.size(edges) > 0
69+
end
70+
end
71+
6572
defp traverse(q, %Graph{out_edges: oe, vertices: vertices} = g, visited, fun, acc) do
6673
case PriorityQueue.pop(q) do
6774
{{:value, v_id}, q1} ->

lib/graph/serializers/dot.ex

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,27 +14,23 @@ defmodule Graph.Serializers.DOT do
1414

1515
defp serialize_nodes(%Graph{vertices: vertices} = g) do
1616
Enum.reduce(vertices, "", fn {id, v}, acc ->
17-
acc <> Serializer.indent(1) <> Serializer.get_vertex_label(g, id, v) <> "\n"
17+
acc <> Serializer.indent(1) <> "#{id}" <> "[label=" <> Serializer.get_vertex_label(g, id, v) <> "]\n"
1818
end)
1919
end
2020

21-
defp serialize_edges(%Graph{type: type, vertices: vertices, out_edges: oe, edges: em} = g) do
21+
defp serialize_edges(%Graph{type: type, vertices: vertices, out_edges: oe, edges: em} = _g) do
2222
edges =
23-
Enum.reduce(vertices, [], fn {id, v}, acc ->
24-
v_label = Serializer.get_vertex_label(g, id, v)
25-
23+
Enum.reduce(vertices, [], fn {id, _v}, acc ->
2624
edges =
2725
oe
2826
|> Map.get(id, MapSet.new())
2927
|> Enum.flat_map(fn id2 ->
30-
v2_label = Serializer.get_vertex_label(g, id2, Map.get(vertices, id2))
31-
3228
Enum.map(Map.fetch!(em, {id, id2}), fn
3329
{nil, weight} ->
34-
{v_label, v2_label, weight}
30+
{id, id2, weight}
3531

3632
{label, weight} ->
37-
{v_label, v2_label, weight, Serializer.encode_label(label)}
33+
{id, id2, weight, Serializer.encode_label(label)}
3834
end)
3935
end)
4036

@@ -47,16 +43,16 @@ defmodule Graph.Serializers.DOT do
4743
arrow = if type == :directed, do: "->", else: "--"
4844

4945
Enum.reduce(edges, "", fn
50-
{v_label, v2_label, weight, edge_label}, acc ->
46+
{v_id, v2_id, weight, edge_label}, acc ->
5147
acc <>
5248
Serializer.indent(1) <>
53-
v_label <>
54-
" #{arrow} " <> v2_label <> " [" <> "label=#{edge_label}; weight=#{weight}" <> "]\n"
49+
"#{v_id}" <>
50+
" #{arrow} " <> "#{v2_id}" <> " [" <> "label=#{edge_label}; weight=#{weight}" <> "]\n"
5551

56-
{v_label, v2_label, weight}, acc ->
52+
{v_id, v2_id, weight}, acc ->
5753
acc <>
5854
Serializer.indent(1) <>
59-
v_label <> " #{arrow} " <> v2_label <> " [" <> "weight=#{weight}" <> "]\n"
55+
"#{v_id}" <> " #{arrow} " <> "#{v2_id}" <> " [" <> "weight=#{weight}" <> "]\n"
6056
end)
6157
end
6258
end

test/graph_test.exs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,12 @@ defmodule GraphTest do
289289
["start", "start_0", 95, 94, 93, 39, 38, 21, 69, 68, "end_0", "end"]
290290
end
291291

292+
test "shortest paths for complex graph using signed weights (negative and positive)" do
293+
g = build_complex_signed_graph()
294+
shortest_paths = Graph.bellman_ford(g, :a)
295+
assert shortest_paths == %{97 => 0, 98 => -1, 99 => 2, 100 => -2, 101 => 1}
296+
end
297+
292298
test "edge undirected graph v1 > v2" do
293299
g = build_basic_undirected_graph()
294300
e1 = Graph.edge(g, :a, :b)
@@ -629,6 +635,18 @@ defmodule GraphTest do
629635
|> Graph.add_edge(:c, :b)
630636
end
631637

638+
defp build_complex_signed_graph do
639+
Graph.new
640+
|> Graph.add_edge(:a, :b, weight: -1)
641+
|> Graph.add_edge(:b, :e, weight: 2)
642+
|> Graph.add_edge(:e, :d, weight: -3)
643+
|> Graph.add_edge(:d, :c, weight: 5)
644+
|> Graph.add_edge(:a, :c, weight: 4)
645+
|> Graph.add_edge(:b, :c, weight: 3)
646+
|> Graph.add_edge(:b, :d, weight: 2)
647+
|> Graph.add_edge(:d, :b, weight: 1)
648+
end
649+
632650
defp build_complex_graph(type \\ :directed) do
633651
Graph.new(type: type)
634652
|> Graph.add_edge(42, 25, weight: 2525)

test/reducer_test.exs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,21 @@ defmodule Graph.Reducer.Test do
3333
expected = [:a, :b, :d, :c, :f, :e, :g]
3434
assert ^expected = Graph.Reducers.Bfs.map(g, fn v -> v end)
3535
end
36+
37+
test "can walk a graph breadth-first, when the starting points had their in-edges deleted" do
38+
g = Graph.new
39+
|> Graph.add_vertices([:a, :b, :c, :d, :e, :f, :g])
40+
|> Graph.add_edge(:a, :b)
41+
|> Graph.add_edge(:a, :d)
42+
|> Graph.add_edge(:b, :c)
43+
|> Graph.add_edge(:b, :d)
44+
|> Graph.add_edge(:c, :e)
45+
|> Graph.add_edge(:d, :f)
46+
|> Graph.add_edge(:f, :g)
47+
|> Graph.add_edge(:b, :a) # Add this edge and then remove it
48+
|> Graph.delete_edge(:b, :a)
49+
50+
expected = [:a, :b, :d, :c, :f, :e, :g]
51+
assert ^expected = Graph.Reducers.Bfs.map(g, fn v -> v end)
52+
end
3653
end

0 commit comments

Comments
 (0)