-
Notifications
You must be signed in to change notification settings - Fork 8
Expand file tree
/
Copy pathtraversal.ex
More file actions
82 lines (64 loc) · 2.86 KB
/
Copy pathtraversal.ex
File metadata and controls
82 lines (64 loc) · 2.86 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
defmodule ExAlgo.Graph.Functional.Traversal do
@moduledoc """
Traversal algorithms for functional inductive graphs.
Unlike traditional graph traversals that use a separate "visited" set, these
inductive traversals rely on the structural `match/2` operation. When a node is
extracted, it returns both the node's context and a *shrunken* graph that no longer
contains that node or its incident edges.
Iterating with this shrunken graph naturally prevents revisiting nodes and
terminates when the graph is empty.
"""
alias ExAlgo.Graph.Functional.Model
@doc """
Performs a Depth-First Search starting from the given node ID(s).
Returns a list of node contexts in the order they were visited.
This is an inductive DFS: each step calls `match/2` to simultaneously
extract a node and obtain the *shrunken* graph without that node.
Revisit prevention comes naturally from the graph shrinking — a node already
visited simply won't be found in the remaining graph.
For directed graphs, only outgoing edges are followed. For undirected graphs,
edges are stored symmetrically so `out_edges` covers all adjacent nodes.
"""
@spec dfs(Model.t(), Model.node_id() | [Model.node_id()]) :: [Model.Context.t()]
def dfs(graph, start_nodes) when is_list(start_nodes) do
do_dfs(graph, start_nodes, [])
end
def dfs(graph, start_node), do: dfs(graph, [start_node])
defp do_dfs(_graph, [], acc), do: Enum.reverse(acc)
defp do_dfs(graph, [current_id | stack], acc) do
case Model.match(graph, current_id) do
{:ok, ctx, remaining_graph} ->
new_stack = neighbors_of(ctx) ++ stack
do_dfs(remaining_graph, new_stack, [ctx | acc])
{:error, :not_found} ->
do_dfs(graph, stack, acc)
end
end
@doc """
Performs a Breadth-First Search starting from the given node ID(s).
Returns a list of node contexts in the order they were visited.
This is an inductive BFS: each step calls `match/2` to extract a node
and obtain the shrunken graph, ensuring nodes are visited at most once.
"""
@spec bfs(Model.t(), Model.node_id() | [Model.node_id()]) :: [Model.Context.t()]
def bfs(graph, start_nodes) when is_list(start_nodes) do
queue = :queue.from_list(start_nodes)
do_bfs(graph, queue, [])
end
def bfs(graph, start_node), do: bfs(graph, [start_node])
defp do_bfs(graph, queue, acc) do
case :queue.out(queue) do
{{:value, current_id}, rest_queue} ->
case Model.match(graph, current_id) do
{:ok, ctx, remaining_graph} ->
new_queue = Enum.reduce(neighbors_of(ctx), rest_queue, &:queue.in/2)
do_bfs(remaining_graph, new_queue, [ctx | acc])
{:error, :not_found} ->
do_bfs(graph, rest_queue, acc)
end
{:empty, _} ->
Enum.reverse(acc)
end
end
defp neighbors_of(%Model.Context{out_edges: edges}), do: Map.keys(edges)
end