Current section
Files
Jump to
Current section
Files
src/yog@pathfinding@matrix.erl
-module(yog@pathfinding@matrix).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/yog/pathfinding/matrix.gleam").
-export([distance_matrix/6]).
-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(
" Optimized distance matrix computation for subsets of nodes.\n"
"\n"
" This module provides an auto-selecting algorithm for computing shortest path\n"
" distances between specified \"points of interest\" (POIs) in a graph. It intelligently\n"
" chooses between Floyd-Warshall, Johnson's, and multiple Dijkstra runs based on\n"
" graph characteristics and POI density.\n"
"\n"
" ## Algorithm Selection\n"
"\n"
" **With negative weights support** (when `with_subtract` is provided):\n"
"\n"
" | Algorithm | When Selected | Complexity |\n"
" |-----------|---------------|------------|\n"
" | [Johnson's](https://en.wikipedia.org/wiki/Johnson%27s_algorithm) | Sparse graphs (E < V²/4) | O(V² log V + VE) then filter |\n"
" | [Floyd-Warshall](https://en.wikipedia.org/wiki/Floyd%E2%80%93Warshall_algorithm) | Dense graphs (E ≥ V²/4) | O(V³) then filter |\n"
"\n"
" **Without negative weights** (when `with_subtract` is `None`):\n"
"\n"
" | Algorithm | When Selected | Complexity |\n"
" |-----------|---------------|------------|\n"
" | [Dijkstra](https://en.wikipedia.org/wiki/Dijkstra%27s_algorithm) × P | Few POIs (P ≤ V/3) | O(P × (V + E) log V) |\n"
" | [Floyd-Warshall](https://en.wikipedia.org/wiki/Floyd%E2%80%93Warshall_algorithm) | Many POIs (P > V/3) | O(V³) then filter |\n"
"\n"
" ## Heuristics\n"
"\n"
" **For graphs with potential negative weights:**\n"
" - Johnson's algorithm is preferred for sparse graphs where E < V²/4\n"
" - Floyd-Warshall is preferred for denser graphs\n"
"\n"
" **For non-negative weights only:**\n"
" - Multiple Dijkstra runs when P ≤ V/3 (few POIs)\n"
" - Floyd-Warshall when P > V/3 (many POIs)\n"
"\n"
" ## Use Cases\n"
"\n"
" - **Game AI**: Pathfinding between key locations (not all nodes)\n"
" - **Logistics**: Distance matrix for delivery stops\n"
" - **Facility location**: Distances between candidate sites\n"
" - **Network analysis**: Selected node pairwise distances\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" // Compute distances only between important waypoints\n"
" let pois = [start, waypoint_a, waypoint_b, goal]\n"
" let distances = matrix.distance_matrix(\n"
" in: graph,\n"
" between: pois,\n"
" with_zero: 0,\n"
" with_add: int.add,\n"
" with_compare: int.compare,\n"
" )\n"
" // Result contains only 4×4 = 16 distances, not full V×V matrix\n"
" ```\n"
"\n"
" ## References\n"
"\n"
" - See `yog/pathfinding/floyd_warshall` for all-pairs algorithm details (O(V³))\n"
" - See `yog/pathfinding/johnson` for sparse all-pairs with negative weights (O(V² log V + VE))\n"
" - See `yog/pathfinding/dijkstra` for single-source algorithm details (O((V+E) log V))\n"
).
-file("src/yog/pathfinding/matrix.gleam", 201).
?DOC(" Helper function to run Floyd-Warshall and filter results to POIs\n").
-spec use_floyd_warshall(
yog@model:graph(any(), VEX),
gleam@set:set(integer()),
VEX,
fun((VEX, VEX) -> VEX),
fun((VEX, VEX) -> gleam@order:order())
) -> {ok, gleam@dict:dict({integer(), integer()}, VEX)} | {error, nil}.
use_floyd_warshall(Graph, Poi_set, Zero, Add, Compare) ->
case yog@pathfinding@floyd_warshall:floyd_warshall(
Graph,
Zero,
Add,
Compare
) of
{error, nil} ->
{error, nil};
{ok, All_distances} ->
Poi_distances = gleam@dict:filter(
All_distances,
fun(Key, _) ->
{From_node, To_node} = Key,
gleam@set:contains(Poi_set, From_node) andalso gleam@set:contains(
Poi_set,
To_node
)
end
),
{ok, Poi_distances}
end.
-file("src/yog/pathfinding/matrix.gleam", 110).
?DOC(
" Computes shortest distances between all pairs of points of interest.\n"
"\n"
" Automatically chooses the best algorithm based on:\n"
" - Whether negative weights are possible (presence of `with_subtract`)\n"
" - Graph sparsity (E relative to V²)\n"
" - POI density (P relative to V)\n"
"\n"
" **Time Complexity:** O(V³), O(V² log V + VE), or O(P × (V + E) log V)\n"
"\n"
" ## Parameters\n"
"\n"
" - `with_subtract`: Optional subtraction function for negative weight support.\n"
" If provided, enables Johnson's algorithm for sparse graphs with negative weights.\n"
" If `None`, assumes non-negative weights and may use Dijkstra.\n"
"\n"
" ## Examples\n"
"\n"
" ```gleam\n"
" // Non-negative weights only (uses Dijkstra or Floyd-Warshall)\n"
" matrix.distance_matrix(\n"
" in: graph,\n"
" between: pois,\n"
" with_zero: 0,\n"
" with_add: int.add,\n"
" with_subtract: None,\n"
" with_compare: int.compare,\n"
" )\n"
"\n"
" // Support negative weights (uses Johnson's or Floyd-Warshall)\n"
" matrix.distance_matrix(\n"
" in: graph,\n"
" between: pois,\n"
" with_zero: 0,\n"
" with_add: int.add,\n"
" with_subtract: Some(int.subtract),\n"
" with_compare: int.compare,\n"
" )\n"
" ```\n"
).
-spec distance_matrix(
yog@model:graph(any(), VEN),
list(integer()),
VEN,
fun((VEN, VEN) -> VEN),
gleam@option:option(fun((VEN, VEN) -> VEN)),
fun((VEN, VEN) -> gleam@order:order())
) -> {ok, gleam@dict:dict({integer(), integer()}, VEN)} | {error, nil}.
distance_matrix(Graph, Points_of_interest, Zero, Add, Subtract, Compare) ->
Num_nodes = maps:size(erlang:element(3, Graph)),
Num_edges = yog@model:edge_count(Graph),
Num_pois = erlang:length(Points_of_interest),
Poi_set = gleam@set:from_list(Points_of_interest),
case Subtract of
{some, Sub_fn} ->
Is_sparse = (Num_edges * 4) < (Num_nodes * Num_nodes),
case Is_sparse of
true ->
case yog@pathfinding@johnson:johnson(
Graph,
Zero,
Add,
Sub_fn,
Compare
) of
{error, nil} ->
{error, nil};
{ok, All_distances} ->
Poi_distances = gleam@dict:filter(
All_distances,
fun(Key, _) ->
{From_node, To_node} = Key,
gleam@set:contains(Poi_set, From_node)
andalso gleam@set:contains(Poi_set, To_node)
end
),
{ok, Poi_distances}
end;
false ->
use_floyd_warshall(Graph, Poi_set, Zero, Add, Compare)
end;
none ->
case (Num_pois * 3) > Num_nodes of
true ->
use_floyd_warshall(Graph, Poi_set, Zero, Add, Compare);
false ->
Result = gleam@list:fold(
Points_of_interest,
maps:new(),
fun(Acc, Source) ->
Distances = yog@pathfinding@dijkstra:single_source_distances(
Graph,
Source,
Zero,
Add,
Compare
),
gleam@list:fold(
Points_of_interest,
Acc,
fun(Acc2, Target) ->
case gleam_stdlib:map_get(Distances, Target) of
{ok, Dist} ->
gleam@dict:insert(
Acc2,
{Source, Target},
Dist
);
{error, nil} ->
Acc2
end
end
)
end
),
{ok, Result}
end
end.