Current section
Files
Jump to
Current section
Files
src/yog@pathfinding@dijkstra.erl
-module(yog@pathfinding@dijkstra).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/yog/pathfinding/dijkstra.gleam").
-export([shortest_path/6, single_source_distances/5, implicit_dijkstra/6, implicit_dijkstra_by/7, fold/7, shortest_path_int/3, shortest_path_float/3, single_source_distances_int/2, single_source_distances_float/2]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
?MODULEDOC(
" [Dijkstra's algorithm](https://en.wikipedia.org/wiki/Dijkstra%27s_algorithm) for \n"
" single-source shortest paths in graphs with non-negative edge weights.\n"
"\n"
" Dijkstra's algorithm finds the shortest path from a source node to all other reachable\n"
" nodes in a graph. It works by maintaining a priority queue of nodes to visit,\n"
" always expanding the node with the smallest known distance.\n"
"\n"
" ## Algorithm\n"
"\n"
" | Algorithm | Function | Complexity | Best For |\n"
" |-----------|----------|------------|----------|\n"
" | [Dijkstra](https://en.wikipedia.org/wiki/Dijkstra%27s_algorithm) (single-target) | `shortest_path/6` | O((V + E) log V) | One-to-one shortest path |\n"
" | [Dijkstra](https://en.wikipedia.org/wiki/Dijkstra%27s_algorithm) (single-source) | `single_source_distances/5` | O((V + E) log V) | One-to-all shortest paths |\n"
" | Implicit Dijkstra | `implicit_dijkstra/6` | O((V + E) log V) | Large/infinite graphs |\n"
"\n"
" ## Key Concepts\n"
"\n"
" - **Greedy Strategy**: Always expands the node with minimum tentative distance\n"
" - **Priority Queue**: Min-heap ordered by current best distance\n"
" - **Relaxation**: Update distances when a shorter path is found\n"
" - **Non-Negative Weights**: Required for correctness (use Bellman-Ford for negative weights)\n"
"\n"
" ## Comparison with Other Algorithms\n"
"\n"
" | Algorithm | Handles Negative Weights | Complexity | Use Case |\n"
" |-----------|-------------------------|------------|----------|\n"
" | Dijkstra | ❌ No | O((V+E) log V) | General shortest paths |\n"
" | A* | ❌ No | O((V+E) log V) | When good heuristic available |\n"
" | Bellman-Ford | ✅ Yes | O(VE) | Negative weights, cycle detection |\n"
" | Floyd-Warshall | ✅ Yes | O(V³) | All-pairs shortest paths |\n"
"\n"
" ## History\n"
"\n"
" Edsger W. Dijkstra published this algorithm in 1959. The original paper described\n"
" it for finding the shortest path between two nodes, but it's commonly used for\n"
" single-source shortest paths to all nodes.\n"
"\n"
" ## Use Cases\n"
"\n"
" - **Network routing**: OSPF, IS-IS protocols use Dijkstra\n"
" - **Map services**: Shortest driving directions\n"
" - **Social networks**: Degrees of separation\n"
" - **Game development**: Shortest path on weighted grids\n"
"\n"
" ## References\n"
"\n"
" - [Wikipedia: Dijkstra's Algorithm](https://en.wikipedia.org/wiki/Dijkstra%27s_algorithm)\n"
" - [Dijkstra's Original Paper (1959)](https://link.springer.com/article/10.1007/BF01386390)\n"
" - [Red Blob Games: Dijkstra Introduction](https://www.redblobgames.com/pathfinding/a-star/introduction.html)\n"
" - [CP-Algorithms: Dijkstra](https://cp-algorithms.com/graph/dijkstra.html)\n"
).
-file("src/yog/pathfinding/dijkstra.gleam", 90).
?DOC(
" Finds the shortest path between two nodes using Dijkstra's algorithm.\n"
"\n"
" Works with non-negative edge weights only.\n"
"\n"
" **Time Complexity:** O((V + E) log V) with heap\n"
"\n"
" ## Parameters\n"
"\n"
" - `zero`: The identity element for addition (e.g., 0 for integers)\n"
" - `add`: Function to add two weights\n"
" - `compare`: Function to compare two weights\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" dijkstra.shortest_path(\n"
" in: graph,\n"
" from: 1,\n"
" to: 5,\n"
" with_zero: 0,\n"
" with_add: int.add,\n"
" with_compare: int.compare\n"
" )\n"
" // => Some(Path([1, 2, 5], 15))\n"
" ```\n"
).
-spec shortest_path(
yog@model:graph(any(), JNF),
integer(),
integer(),
JNF,
fun((JNF, JNF) -> JNF),
fun((JNF, JNF) -> gleam@order:order())
) -> gleam@option:option(yog@pathfinding@path:path(JNF)).
shortest_path(Graph, Start, Goal, Zero, Add, Compare) ->
yog@pathfinding@a_star:a_star(
Graph,
Start,
Goal,
Zero,
Add,
Compare,
fun(_, _) -> Zero end
).
-file("src/yog/pathfinding/dijkstra.gleam", 135).
-spec do_single_source_dijkstra(
yog@model:graph(any(), JNR),
yog@internal@pairing_heap:heap({JNR, integer()}),
gleam@dict:dict(integer(), JNR),
fun((JNR, JNR) -> JNR),
fun((JNR, JNR) -> gleam@order:order())
) -> gleam@dict:dict(integer(), JNR).
do_single_source_dijkstra(Graph, Frontier, Distances, Add, Compare) ->
case yog@internal@priority_queue:pop(Frontier) of
{error, nil} ->
Distances;
{ok, {{Dist, Current}, Rest_frontier}} ->
Should_explore = yog@internal@util:should_explore_node(
Distances,
Current,
Dist,
Compare
),
case Should_explore of
false ->
do_single_source_dijkstra(
Graph,
Rest_frontier,
Distances,
Add,
Compare
);
true ->
New_distances = gleam@dict:insert(Distances, Current, Dist),
Next_frontier = begin
_pipe = yog@model:successors(Graph, Current),
gleam@list:fold(
_pipe,
Rest_frontier,
fun(H, Neighbor) ->
{Next_id, Weight} = Neighbor,
yog@internal@priority_queue:push(
H,
{Add(Dist, Weight), Next_id}
)
end
)
end,
do_single_source_dijkstra(
Graph,
Next_frontier,
New_distances,
Add,
Compare
)
end
end.
-file("src/yog/pathfinding/dijkstra.gleam", 121).
?DOC(
" Computes shortest distances from a source node to all reachable nodes.\n"
"\n"
" Returns a dictionary mapping each reachable node to its shortest distance\n"
" from the source. Unreachable nodes are not included in the result.\n"
"\n"
" **Time Complexity:** O((V + E) log V) with heap\n"
"\n"
" ## Parameters\n"
"\n"
" - `zero`: The identity element for addition (e.g., 0 for integers)\n"
" - `add`: Function to add two weights\n"
" - `compare`: Function to compare two weights\n"
).
-spec single_source_distances(
yog@model:graph(any(), JNL),
integer(),
JNL,
fun((JNL, JNL) -> JNL),
fun((JNL, JNL) -> gleam@order:order())
) -> gleam@dict:dict(integer(), JNL).
single_source_distances(Graph, Source, Zero, Add, Compare) ->
Frontier = begin
_pipe = yog@internal@priority_queue:new(
fun(A, B) ->
yog@internal@util:compare_distance_frontier(A, B, Compare)
end
),
yog@internal@priority_queue:push(_pipe, {Zero, Source})
end,
do_single_source_dijkstra(Graph, Frontier, maps:new(), Add, Compare).
-file("src/yog/pathfinding/dijkstra.gleam", 197).
?DOC(
" Finds the shortest path in an implicit graph using Dijkstra's algorithm.\n"
"\n"
" Instead of a materialized `Graph`, this uses a `successors_with_cost` function\n"
" to compute weighted neighbors on demand.\n"
"\n"
" Returns the shortest distance to any state satisfying `is_goal`, or `None`\n"
" if no goal state is reachable.\n"
"\n"
" **Time Complexity:** O((V + E) log V) where V is visited states and E is explored transitions\n"
"\n"
" ## Parameters\n"
"\n"
" - `successors_with_cost`: Function that generates weighted successors for a state\n"
" - `is_goal`: Predicate that identifies goal states\n"
" - `zero`: The identity element for addition (e.g., 0 for integers)\n"
" - `add`: Function to add two costs\n"
" - `compare`: Function to compare two costs\n"
).
-spec implicit_dijkstra(
JNZ,
fun((JNZ) -> list({JNZ, JOA})),
fun((JNZ) -> boolean()),
JOA,
fun((JOA, JOA) -> JOA),
fun((JOA, JOA) -> gleam@order:order())
) -> gleam@option:option(JOA).
implicit_dijkstra(Start, Successors, Is_goal, Zero, Add, Compare) ->
yog@pathfinding@a_star:implicit_a_star(
Start,
Successors,
Is_goal,
fun(_) -> Zero end,
Zero,
Add,
Compare
).
-file("src/yog/pathfinding/dijkstra.gleam", 222).
?DOC(
" Like `implicit_dijkstra`, but deduplicates visited states by a custom key.\n"
"\n"
" Essential when your state carries extra data beyond what defines identity.\n"
" The `visited_by` function extracts the deduplication key from each state.\n"
"\n"
" **Time Complexity:** O((V + E) log V) where V and E are measured in unique *keys*\n"
).
-spec implicit_dijkstra_by(
JOD,
fun((JOD) -> list({JOD, JOE})),
fun((JOD) -> any()),
fun((JOD) -> boolean()),
JOE,
fun((JOE, JOE) -> JOE),
fun((JOE, JOE) -> gleam@order:order())
) -> gleam@option:option(JOE).
implicit_dijkstra_by(Start, Successors, Key_fn, Is_goal, Zero, Add, Compare) ->
yog@pathfinding@a_star:implicit_a_star_by(
Start,
Successors,
Key_fn,
Is_goal,
fun(_) -> Zero end,
Zero,
Add,
Compare
).
-file("src/yog/pathfinding/dijkstra.gleam", 265).
-spec do_fold(
yog@internal@pairing_heap:heap({JSL, JSH}),
gleam@dict:dict(JSH, JSL),
JOT,
fun((JSH) -> list({JSH, JSI})),
fun((JSL, JSI) -> JSL),
fun((JSL, JSL) -> gleam@order:order()),
fun((JOT, JSH, JSL) -> {yog@traversal:walk_control(), JOT})
) -> JOT.
do_fold(Frontier, Best, Acc, Successors, Add, Compare, Folder) ->
case yog@internal@priority_queue:pop(Frontier) of
{error, nil} ->
Acc;
{ok, {{Cost, Node}, Rest}} ->
Is_stale = case gleam_stdlib:map_get(Best, Node) of
{ok, Prev} ->
Compare(Prev, Cost) /= gt;
_ ->
false
end,
case Is_stale of
true ->
do_fold(Rest, Best, Acc, Successors, Add, Compare, Folder);
false ->
New_best = gleam@dict:insert(Best, Node, Cost),
{Control, New_acc} = Folder(Acc, Node, Cost),
case Control of
halt ->
New_acc;
stop ->
do_fold(
Rest,
New_best,
New_acc,
Successors,
Add,
Compare,
Folder
);
continue ->
Next_frontier = gleam@list:fold(
Successors(Node),
Rest,
fun(Q, Neighbor) ->
{Nb_node, Edge_cost} = Neighbor,
New_cost = Add(Cost, Edge_cost),
Is_worse = case gleam_stdlib:map_get(
New_best,
Nb_node
) of
{ok, Prev_cost} ->
Compare(Prev_cost, New_cost) /= gt;
_ ->
false
end,
case Is_worse of
true ->
Q;
false ->
yog@internal@priority_queue:push(
Q,
{New_cost, Nb_node}
)
end
end
),
do_fold(
Next_frontier,
New_best,
New_acc,
Successors,
Add,
Compare,
Folder
)
end
end
end.
-file("src/yog/pathfinding/dijkstra.gleam", 249).
?DOC(
" Folds over an implicit weighted graph using Dijkstra's algorithm.\n"
"\n"
" Like `implicit_dijkstra` but visits nodes in order of increasing cost and\n"
" accumulates state. Provides control over traversal via `WalkControl`.\n"
"\n"
" **Time Complexity:** O((V + E) log V)\n"
).
-spec fold(
JOI,
JOJ,
fun((JOI) -> list({JOI, JOK})),
JOK,
fun((JOK, JOK) -> JOK),
fun((JOK, JOK) -> gleam@order:order()),
fun((JOJ, JOI, JOK) -> {yog@traversal:walk_control(), JOJ})
) -> JOJ.
fold(Start, Acc, Successors, Zero, Add, Compare, Folder) ->
Frontier = begin
_pipe = yog@internal@priority_queue:new(
fun(A, B) -> Compare(erlang:element(1, A), erlang:element(1, B)) end
),
yog@internal@priority_queue:push(_pipe, {Zero, Start})
end,
do_fold(Frontier, maps:new(), Acc, Successors, Add, Compare, Folder).
-file("src/yog/pathfinding/dijkstra.gleam", 346).
?DOC(
" Finds the shortest path using **integer weights**.\n"
"\n"
" This is a convenience wrapper around `shortest_path` that uses:\n"
" - `0` as the zero element\n"
" - `int.add` for addition\n"
" - `int.compare` for comparison\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" // Much cleaner than the full explicit version\n"
" dijkstra.shortest_path_int(graph, from: 1, to: 5)\n"
" // => Some(Path([1, 2, 5], 15))\n"
"\n"
" // Equivalent explicit call:\n"
" // dijkstra.shortest_path(\n"
" // graph, from: 1, to: 5,\n"
" // with_zero: 0,\n"
" // with_add: int.add,\n"
" // with_compare: int.compare\n"
" // )\n"
" ```\n"
"\n"
" ## When to Use\n"
"\n"
" Use this for graphs with `Int` edge weights (hop counts, distances in meters,\n"
" costs in cents, etc.). For custom weight types (Money, Distance, etc.), use\n"
" the full `shortest_path` function with your own semiring operations.\n"
).
-spec shortest_path_int(yog@model:graph(any(), integer()), integer(), integer()) -> gleam@option:option(yog@pathfinding@path:path(integer())).
shortest_path_int(Graph, Start, Goal) ->
shortest_path(
Graph,
Start,
Goal,
0,
fun gleam@int:add/2,
fun gleam@int:compare/2
).
-file("src/yog/pathfinding/dijkstra.gleam", 381).
?DOC(
" Finds the shortest path using **float weights**.\n"
"\n"
" This is a convenience wrapper around `shortest_path` that uses:\n"
" - `0.0` as the zero element\n"
" - `float.add` for addition\n"
" - `float.compare` for comparison\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" dijkstra.shortest_path_float(graph, from: 1, to: 5)\n"
" // => Some(Path([1, 2, 5], 15.5))\n"
" ```\n"
"\n"
" ## When to Use\n"
"\n"
" Use this for graphs with `Float` edge weights (probabilities, distances in\n"
" kilometers with decimal precision, continuous costs, etc.). Note that float\n"
" arithmetic has precision limitations - for exact calculations, prefer `Int`\n"
" weights (e.g., store cents instead of dollars).\n"
).
-spec shortest_path_float(yog@model:graph(any(), float()), integer(), integer()) -> gleam@option:option(yog@pathfinding@path:path(float())).
shortest_path_float(Graph, Start, Goal) ->
shortest_path(
Graph,
Start,
Goal,
+0.0,
fun gleam@float:add/2,
fun gleam@float:compare/2
).
-file("src/yog/pathfinding/dijkstra.gleam", 399).
?DOC(
" Computes shortest distances using **integer weights**.\n"
"\n"
" Convenience wrapper for `single_source_distances` with `Int` weights.\n"
).
-spec single_source_distances_int(yog@model:graph(any(), integer()), integer()) -> gleam@dict:dict(integer(), integer()).
single_source_distances_int(Graph, Source) ->
single_source_distances(
Graph,
Source,
0,
fun gleam@int:add/2,
fun gleam@int:compare/2
).
-file("src/yog/pathfinding/dijkstra.gleam", 415).
?DOC(
" Computes shortest distances using **float weights**.\n"
"\n"
" Convenience wrapper for `single_source_distances` with `Float` weights.\n"
).
-spec single_source_distances_float(yog@model:graph(any(), float()), integer()) -> gleam@dict:dict(integer(), float()).
single_source_distances_float(Graph, Source) ->
single_source_distances(
Graph,
Source,
+0.0,
fun gleam@float:add/2,
fun gleam@float:compare/2
).