Skip to main content

Dijkstra

Dijkstra's shortest-path and priority-ordered traversal algorithms.

Dijkstra's algorithm finds the shortest (lowest total weight) path from a source vertex to one or all reachable vertices in a graph with non-negative edge weights. It uses a min-heap priority queue to always expand the cheapest known vertex next.

Non-negative weights only

Dijkstra's algorithm produces incorrect results if any edge weight is negative. Use BellmanFord when negative weights are possible.

Priority traversal

Dijkstra.toIterator and Dijkstra.toArray let you traverse all reachable vertices in ascending accumulated-cost order — useful for budget-limited flood fill, range queries, and influence-map generation:

for vertex, costFromStart in Dijkstra.toIterator(origin, getNeighbors, getWeight) do
    if costFromStart > budget then break end
    markInRange(vertex)
end

All functions accept either callback form for GetNeighbors — see GraphUtil for details.

Example graph

--      2       1
--  A ───── B ───── D
--  │               │
--  3               5
--  │               │
--  C ───────────────┘

local weights = {
    A = { B = 2, C = 3 },
    B = { A = 2, D = 1 },
    C = { A = 3, D = 5 },
    D = { B = 1, C = 5 },
}

local function getNeighbors(v) return weights[v] and (function()
    local keys = {}
    for k in pairs(weights[v]) do table.insert(keys, k) end
    return keys
end)() or {} end

local function getWeight(from, to) return weights[from][to] end

Functions

shortestPath

directedundirected
Dijkstra.shortestPath(
startV,
goalV,
getNeighborsGetNeighbors<V>,
getWeightGetWeight<V>,
shouldStopShouldStop<V>?
) → PathResult<V>?

Finds the shortest weighted path from start to goal and returns a PathResult, or nil if goal is unreachable.

local result = Dijkstra.shortestPath("A", "D", getNeighbors, getWeight)
-- result.path == { "A", "B", "D" }
-- result.cost == 3   (2 + 1)

Pass an optional ShouldStop predicate to abort a long-running search — checked once per expanded vertex (after the goal check), returning true stops the search and returns nil:

-- Stop once the cheapest frontier vertex exceeds a budget of 500.
local result = Dijkstra.shortestPath(start, goal, getNeighbors, getWeight, function(_vertex, cost)
    return cost > 500
end)

Time complexity: O((V + E) log V) with a binary min-heap.

bidirectionalShortestPath

directedundirected
Dijkstra.bidirectionalShortestPath(
startV,
goalV,
getNeighborsGetNeighbors<V>,
getWeightGetWeight<V>,
getReversedNeighborsGetNeighbors<V>?,
shouldStopShouldStop<V>?
) → PathResult<V>?

Finds the shortest weighted path between start and goal by running Dijkstra from both ends simultaneously — alternately expanding whichever heap has the cheaper next vertex — and stopping as soon as the two searches are provably guaranteed to have met at the shortest possible point. Returns the same PathResult as Dijkstra.shortestPath, or nil if goal is unreachable.

local result = Dijkstra.bidirectionalShortestPath("A", "D", getNeighbors, getWeight)
-- result.path == { "A", "B", "D" }
-- result.cost == 3

:::caution Directed graphs Growing a search backward from goal requires walking edges in reverse. By default this function reuses getNeighbors for both directions, which is only correct for undirected graphs. For a directed graph, pass a callback that returns the predecessors of a vertex (i.e. getReversedNeighbors(v) returns every u such that u → v is an edge) — the edge weight is still looked up as getWeight(u, v), so no separate reverse-weight callback is needed:

-- Directed graph where reverseAdj[v] lists v's predecessors
local function getReversedNeighbors(v)
    return reverseAdj[v]
end

local result = Dijkstra.bidirectionalShortestPath(
    "A", "D", getNeighbors, getWeight, getReversedNeighbors
)

:::

An optional ShouldStop predicate (passed after getReversedNeighbors) aborts the search and returns nil — it is checked each time either side settles a vertex.

Time complexity: O((V + E) log V), but typically explores far fewer vertices in practice since each side only has to reach roughly halfway.

distances

directedundirected
Dijkstra.distances(
startV,
getNeighborsGetNeighbors<V>,
getWeightGetWeight<V>,
shouldStopShouldStop<V>?
) → {[V]number}

Runs Dijkstra from start to completion and returns a map of every reachable vertex to its shortest distance from start. Unreachable vertices are absent from the table.

local d = Dijkstra.distances("A", getNeighbors, getWeight)
print(d["B"]) -- 2
print(d["C"]) -- 3
print(d["D"]) -- 3

Pass an optional ShouldStop predicate to abort a long-running flood — checked once per settled vertex. Unlike the path-finders, on early stop this returns the partial distance map accumulated so far (every vertex settled before the stop is present and final):

-- Only compute distances out to a budget of 10.
local d = Dijkstra.distances(start, getNeighbors, getWeight, function(_vertex, cost)
    return cost > 10
end)

Time complexity: O((V + E) log V) with a binary min-heap.

toIterator

directedundirected
Dijkstra.toIterator(
startV,
getNeighborsGetNeighbors<V>,
getWeightGetWeight<V>
) → KVIterator<V,number>

Returns a lazy iterator that yields (vertex, costFromStart) pairs in ascending accumulated-cost order — every vertex is visited before any vertex that is reachable through it at higher cost.

The iterator drives the heap expansion internally via a closure; no coroutines are used. You can break early to stop expansion, which is the key advantage over Dijkstra.distances.

Aliased as Dijkstra.forEach.

-- Visit all nodes within a travel budget of 5
for vertex, cost in Dijkstra.toIterator(origin, getNeighbors, getWeight) do
    if cost > 5 then break end
    table.insert(inRange, vertex)
end
for-loop usage
for vertex, cost in Dijkstra.toIterator(start, getNeighbors, getWeight) do
    print(vertex, cost)
end

Time complexity per call: amortised O(degree(V) · log V)

forEach

directedundirected
Dijkstra.forEach(
startV,
getNeighborsGetNeighbors<V>,
getWeightGetWeight<V>
) → KVIterator<V,number>

toArray

directedundirected
Dijkstra.toArray(
startV,
getNeighborsGetNeighbors<V>,
getWeightGetWeight<V>
) → {VisitRecord<V>}

Runs Dijkstra from start to completion and returns an array of VisitRecord values in ascending accumulated-cost order.

local records = Dijkstra.toArray("A", getNeighbors, getWeight)
for _, record in records do
    print(record.vertex, record.cost)
end
-- A  0
-- B  2
-- C  3
-- D  3

Time complexity: O((V + E) log V) with a binary min-heap.

Show raw api
{
    "functions": [
        {
            "name": "shortestPath",
            "desc": "Finds the **shortest weighted path** from `start` to `goal` and returns a\n[PathResult](/api/GraphUtil#PathResult), or `nil` if `goal` is unreachable.\n\n```lua\nlocal result = Dijkstra.shortestPath(\"A\", \"D\", getNeighbors, getWeight)\n-- result.path == { \"A\", \"B\", \"D\" }\n-- result.cost == 3   (2 + 1)\n```\n\nPass an optional [ShouldStop](/api/GraphUtil#ShouldStop) predicate to abort a\nlong-running search — checked once per expanded vertex (after the goal check),\nreturning `true` stops the search and returns `nil`:\n\n```lua\n-- Stop once the cheapest frontier vertex exceeds a budget of 500.\nlocal result = Dijkstra.shortestPath(start, goal, getNeighbors, getWeight, function(_vertex, cost)\n    return cost > 500\nend)\n```\n\n*Time complexity:* `O((V + E) log V)` with a binary min-heap.",
            "params": [
                {
                    "name": "start",
                    "desc": "",
                    "lua_type": "V"
                },
                {
                    "name": "goal",
                    "desc": "",
                    "lua_type": "V"
                },
                {
                    "name": "getNeighbors",
                    "desc": "",
                    "lua_type": "GetNeighbors<V>"
                },
                {
                    "name": "getWeight",
                    "desc": "",
                    "lua_type": "GetWeight<V>"
                },
                {
                    "name": "shouldStop",
                    "desc": "",
                    "lua_type": "ShouldStop<V>?\n"
                }
            ],
            "returns": [
                {
                    "desc": "",
                    "lua_type": "PathResult<V>?\n"
                }
            ],
            "function_type": "static",
            "tags": [
                "directed",
                "undirected"
            ],
            "source": {
                "line": 130,
                "path": "lib/graphutil/src/Dijkstra.luau"
            }
        },
        {
            "name": "bidirectionalShortestPath",
            "desc": "Finds the **shortest weighted path** between `start` and `goal` by\nrunning Dijkstra from both ends simultaneously — alternately expanding\nwhichever heap has the cheaper next vertex — and stopping as soon as the\ntwo searches are provably guaranteed to have met at the shortest\npossible point. Returns the same [PathResult](/api/GraphUtil#PathResult)\nas [`Dijkstra.shortestPath`](#shortestPath), or `nil` if `goal` is\nunreachable.\n\n```lua\nlocal result = Dijkstra.bidirectionalShortestPath(\"A\", \"D\", getNeighbors, getWeight)\n-- result.path == { \"A\", \"B\", \"D\" }\n-- result.cost == 3\n```\n\n:::caution Directed graphs\nGrowing a search backward from `goal` requires walking edges in\nreverse. By default this function reuses `getNeighbors` for both\ndirections, which is only correct for **undirected** graphs. For a\ndirected graph, pass a callback that returns the *predecessors* of a\nvertex (i.e. `getReversedNeighbors(v)` returns every `u` such that\n`u → v` is an edge) — the edge weight is still looked up as\n`getWeight(u, v)`, so no separate reverse-weight callback is needed:\n\n```lua\n-- Directed graph where reverseAdj[v] lists v's predecessors\nlocal function getReversedNeighbors(v)\n    return reverseAdj[v]\nend\n\nlocal result = Dijkstra.bidirectionalShortestPath(\n    \"A\", \"D\", getNeighbors, getWeight, getReversedNeighbors\n)\n```\n:::\n\nAn optional [ShouldStop](/api/GraphUtil#ShouldStop) predicate (passed after\n`getReversedNeighbors`) aborts the search and returns `nil` — it is checked\neach time either side settles a vertex.\n\n*Time complexity:* `O((V + E) log V)`, but typically explores far fewer\nvertices in practice since each side only has to reach roughly halfway.",
            "params": [
                {
                    "name": "start",
                    "desc": "",
                    "lua_type": "V"
                },
                {
                    "name": "goal",
                    "desc": "",
                    "lua_type": "V"
                },
                {
                    "name": "getNeighbors",
                    "desc": "",
                    "lua_type": "GetNeighbors<V>"
                },
                {
                    "name": "getWeight",
                    "desc": "",
                    "lua_type": "GetWeight<V>"
                },
                {
                    "name": "getReversedNeighbors",
                    "desc": "",
                    "lua_type": "GetNeighbors<V>?"
                },
                {
                    "name": "shouldStop",
                    "desc": "",
                    "lua_type": "ShouldStop<V>?\n"
                }
            ],
            "returns": [
                {
                    "desc": "",
                    "lua_type": "PathResult<V>?\n"
                }
            ],
            "function_type": "static",
            "tags": [
                "directed",
                "undirected"
            ],
            "source": {
                "line": 230,
                "path": "lib/graphutil/src/Dijkstra.luau"
            }
        },
        {
            "name": "distances",
            "desc": "Runs Dijkstra from `start` to completion and returns a map of every\nreachable vertex to its shortest distance from `start`.  Unreachable\nvertices are absent from the table.\n\n```lua\nlocal d = Dijkstra.distances(\"A\", getNeighbors, getWeight)\nprint(d[\"B\"]) -- 2\nprint(d[\"C\"]) -- 3\nprint(d[\"D\"]) -- 3\n```\n\nPass an optional [ShouldStop](/api/GraphUtil#ShouldStop) predicate to abort a\nlong-running flood — checked once per settled vertex. Unlike the path-finders,\non early stop this returns the **partial** distance map accumulated so far\n(every vertex settled before the stop is present and final):\n\n```lua\n-- Only compute distances out to a budget of 10.\nlocal d = Dijkstra.distances(start, getNeighbors, getWeight, function(_vertex, cost)\n    return cost > 10\nend)\n```\n\n*Time complexity:* `O((V + E) log V)` with a binary min-heap.",
            "params": [
                {
                    "name": "start",
                    "desc": "",
                    "lua_type": "V"
                },
                {
                    "name": "getNeighbors",
                    "desc": "",
                    "lua_type": "GetNeighbors<V>"
                },
                {
                    "name": "getWeight",
                    "desc": "",
                    "lua_type": "GetWeight<V>"
                },
                {
                    "name": "shouldStop",
                    "desc": "",
                    "lua_type": "ShouldStop<V>?\n"
                }
            ],
            "returns": [
                {
                    "desc": "",
                    "lua_type": "{ [V]: number }\n"
                }
            ],
            "function_type": "static",
            "tags": [
                "directed",
                "undirected"
            ],
            "source": {
                "line": 390,
                "path": "lib/graphutil/src/Dijkstra.luau"
            }
        },
        {
            "name": "toIterator",
            "desc": "Returns a lazy iterator that yields `(vertex, costFromStart)` pairs in\n**ascending accumulated-cost order** — every vertex is visited before any\nvertex that is reachable through it at higher cost.\n\nThe iterator drives the heap expansion internally via a closure; no\ncoroutines are used.  You can `break` early to stop expansion, which is\nthe key advantage over [`Dijkstra.distances`](#distances).\n\nAliased as [`Dijkstra.forEach`](#forEach).\n\n```lua\n-- Visit all nodes within a travel budget of 5\nfor vertex, cost in Dijkstra.toIterator(origin, getNeighbors, getWeight) do\n    if cost > 5 then break end\n    table.insert(inRange, vertex)\nend\n```\n\n:::tip for-loop usage\n```lua\nfor vertex, cost in Dijkstra.toIterator(start, getNeighbors, getWeight) do\n    print(vertex, cost)\nend\n```\n:::\n\n*Time complexity per call:* amortised `O(degree(V) · log V)`",
            "params": [
                {
                    "name": "start",
                    "desc": "",
                    "lua_type": "V"
                },
                {
                    "name": "getNeighbors",
                    "desc": "",
                    "lua_type": "GetNeighbors<V>"
                },
                {
                    "name": "getWeight",
                    "desc": "",
                    "lua_type": "GetWeight<V>"
                }
            ],
            "returns": [
                {
                    "desc": "",
                    "lua_type": "KVIterator<V, number>\n"
                }
            ],
            "function_type": "static",
            "tags": [
                "directed",
                "undirected"
            ],
            "source": {
                "line": 465,
                "path": "lib/graphutil/src/Dijkstra.luau"
            }
        },
        {
            "name": "forEach",
            "desc": "Alias of [`Dijkstra.toIterator`](#toIterator).",
            "params": [
                {
                    "name": "start",
                    "desc": "",
                    "lua_type": "V"
                },
                {
                    "name": "getNeighbors",
                    "desc": "",
                    "lua_type": "GetNeighbors<V>"
                },
                {
                    "name": "getWeight",
                    "desc": "",
                    "lua_type": "GetWeight<V>"
                }
            ],
            "returns": [
                {
                    "desc": "",
                    "lua_type": "KVIterator<V, number>"
                }
            ],
            "function_type": "static",
            "tags": [
                "directed",
                "undirected"
            ],
            "source": {
                "line": 512,
                "path": "lib/graphutil/src/Dijkstra.luau"
            }
        },
        {
            "name": "toArray",
            "desc": "Runs Dijkstra from `start` to completion and returns an array of\n[VisitRecord](/api/Dijkstra#VisitRecord) values in ascending accumulated-cost\norder.\n\n```lua\nlocal records = Dijkstra.toArray(\"A\", getNeighbors, getWeight)\nfor _, record in records do\n    print(record.vertex, record.cost)\nend\n-- A  0\n-- B  2\n-- C  3\n-- D  3\n```\n\n*Time complexity:* `O((V + E) log V)` with a binary min-heap.",
            "params": [
                {
                    "name": "start",
                    "desc": "",
                    "lua_type": "V"
                },
                {
                    "name": "getNeighbors",
                    "desc": "",
                    "lua_type": "GetNeighbors<V>"
                },
                {
                    "name": "getWeight",
                    "desc": "",
                    "lua_type": "GetWeight<V>"
                }
            ],
            "returns": [
                {
                    "desc": "",
                    "lua_type": "{ VisitRecord<V> }\n"
                }
            ],
            "function_type": "static",
            "tags": [
                "directed",
                "undirected"
            ],
            "source": {
                "line": 536,
                "path": "lib/graphutil/src/Dijkstra.luau"
            }
        }
    ],
    "properties": [],
    "types": [],
    "name": "Dijkstra",
    "desc": "Dijkstra's shortest-path and priority-ordered traversal algorithms.\n\nDijkstra's algorithm finds the shortest (lowest total weight) path from a\nsource vertex to one or all reachable vertices in a graph with\n**non-negative edge weights**.  It uses a min-heap priority queue to always\nexpand the cheapest known vertex next.\n\n:::caution Non-negative weights only\nDijkstra's algorithm produces incorrect results if any edge weight is\nnegative.  Use [BellmanFord](/api/BellmanFord) when negative weights are\npossible.\n:::\n\n:::tip Priority traversal\n[`Dijkstra.toIterator`](#toIterator) and [`Dijkstra.toArray`](#toArray) let\nyou traverse **all** reachable vertices in ascending accumulated-cost\norder — useful for budget-limited flood fill, range queries, and\ninfluence-map generation:\n```lua\nfor vertex, costFromStart in Dijkstra.toIterator(origin, getNeighbors, getWeight) do\n    if costFromStart > budget then break end\n    markInRange(vertex)\nend\n```\n:::\n\nAll functions accept either callback form for `GetNeighbors` — see\n[GraphUtil](/api/GraphUtil) for details.\n\n### Example graph\n\n```lua\n--      2       1\n--  A ───── B ───── D\n--  │               │\n--  3               5\n--  │               │\n--  C ───────────────┘\n\nlocal weights = {\n    A = { B = 2, C = 3 },\n    B = { A = 2, D = 1 },\n    C = { A = 3, D = 5 },\n    D = { B = 1, C = 5 },\n}\n\nlocal function getNeighbors(v) return weights[v] and (function()\n    local keys = {}\n    for k in pairs(weights[v]) do table.insert(keys, k) end\n    return keys\nend)() or {} end\n\nlocal function getWeight(from, to) return weights[from][to] end\n```",
    "source": {
        "line": 63,
        "path": "lib/graphutil/src/Dijkstra.luau"
    }
}