Skip to main content

BFS

Breadth-First Search traversal and unweighted shortest-path algorithms.

BFS explores a graph level-by-level outward from a start vertex, visiting all neighbours at the current depth before moving deeper. This property makes it the correct choice for unweighted shortest paths (fewest hops). For weighted shortest paths, use Dijkstra or AStar.

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

Example graph

--  A ─── B ─── D
--  │           │
--  C ──────────┘

local adj = {
    A = {"B", "C"},
    B = {"A", "D"},
    C = {"A", "D"},
    D = {"B", "C"},
}

local function getNeighbors(v)
    return adj[v]
end

Functions

toArray

directedundirected
BFS.toArray(
startV,
getNeighborsGetNeighbors<V>
) → {V}

Visits every vertex reachable from start in breadth-first order and returns them as an ordered array.

local visited = BFS.toArray("A", getNeighbors)
-- { "A", "B", "C", "D" }  (BFS level order)

Time complexity: O(V + E)

toSet

directedundirected
BFS.toSet(
startV,
getNeighborsGetNeighbors<V>
) → {[V]boolean}

Visits every vertex reachable from start in breadth-first order and returns a set { [V]: true } of all visited vertices. Useful when you only need a reachability check rather than the traversal order itself.

local reachable = BFS.toSet("A", getNeighbors)
print(reachable["D"]) -- true
print(reachable["Z"]) -- nil (unreachable)

Time complexity: O(V + E)

toIterator

directedundirected
BFS.toIterator(
startV,
getNeighborsGetNeighbors<V>
) → KIterator<V>

Returns a lazy iterator that yields vertices one at a time in breadth-first order. The iterator drives the BFS internally via a closure — no coroutines are used.

Aliased as BFS.forEach.

local next = BFS.toIterator("A", getNeighbors)
print(next()) -- "A"
print(next()) -- "B"
print(next()) -- "C"
-- ...
print(next()) -- nil  (exhausted)
for-loop usage

The iterator is compatible with a plain for loop since Luau supports for x in fn do for () -> T? iterators:

for vertex in BFS.toIterator("A", getNeighbors) do
    print(vertex)
end

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

forEach

directedundirected
BFS.forEach(
startV,
getNeighborsGetNeighbors<V>
) → KIterator<V>

Alias of BFS.toIterator.

shortestPath

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

Finds the shortest path (fewest hops / edges) from start to goal using BFS. Returns a PathResult where cost is the hop count, or nil if goal is unreachable from start.

This is the correct function to use when all edges have equal (or no) weight. For weighted shortest paths use Dijkstra.shortestPath.

local result = BFS.shortestPath("A", "D", getNeighbors)
-- result.path == { "A", "B", "D" }  (or { "A", "C", "D" })
-- result.cost == 2

Pass an optional ShouldStop predicate to abort a long-running search — checked once per dequeued vertex, receiving that vertex's hop-distance from start as cost. Returning true stops the search and returns nil:

-- Search no deeper than 6 hops.
local result = BFS.shortestPath("A", "D", getNeighbors, function(_vertex, depth)
    return depth >= 6
end)

Time complexity: O(V + E)

bidirectionalShortestPath

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

Finds the shortest path (fewest hops) between start and goal by growing a BFS frontier from both ends simultaneously and stopping as soon as the two frontiers are provably guaranteed to have met at the shortest possible point. Returns the same PathResult as BFS.shortestPath, or nil if goal is unreachable.

On large, densely-branching graphs this explores far fewer vertices than a single-directional search — each side only needs to reach roughly half the hop-distance, so the total number of vertices visited grows with the branching factor to the power of half the distance instead of the full distance.

local result = BFS.bidirectionalShortestPath("A", "D", getNeighbors)
-- result.path == { "A", "B", "D" }  (or { "A", "C", "D" })
-- result.cost == 2

:::caution Directed graphs Growing a frontier 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 third callback that returns the predecessors of a vertex (i.e. getReversedNeighbors(v) returns every u such that u → v is an edge):

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

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

:::

An optional ShouldStop predicate (passed after getReversedNeighbors) aborts the search and returns nil — it is checked each time either side dequeues a vertex, receiving that vertex's hop-distance from its own root as cost.

Time complexity: O(V + E) worst case, but typically O(b^(d/2)) for a graph with branching factor b and hop-distance d between start and goal.

toParentMap

directedundirected
BFS.toParentMap(
startV,
getNeighborsGetNeighbors<V>
) → {[V]V}

Performs a full BFS from start and returns a parent map — a table where every reachable vertex maps to the vertex it was discovered from. The start vertex maps to itself.

This gives you the BFS spanning tree in one pass, letting you reconstruct the shortest hop-path to any reachable vertex without running a separate search per target:

local function pathTo(parentMap, start, goal)
    if not parentMap[goal] then return nil end  -- unreachable
    local path = {}
    local v = goal
    while v ~= start do
        table.insert(path, 1, v)
        v = parentMap[v]
    end
    table.insert(path, 1, start)
    return path
end

local parents = BFS.toParentMap("A", getNeighbors)
print(pathTo(parents, "A", "D"))  -- { "A", "B", "D" }
print(pathTo(parents, "A", "C"))  -- { "A", "C" }

Time complexity: O(V + E)

Show raw api
{
    "functions": [
        {
            "name": "toArray",
            "desc": "Visits every vertex reachable from `start` in breadth-first order and\nreturns them as an ordered array.\n\n```lua\nlocal visited = BFS.toArray(\"A\", getNeighbors)\n-- { \"A\", \"B\", \"C\", \"D\" }  (BFS level order)\n```\n\n*Time complexity:* `O(V + E)`",
            "params": [
                {
                    "name": "start",
                    "desc": "",
                    "lua_type": "V"
                },
                {
                    "name": "getNeighbors",
                    "desc": "",
                    "lua_type": "GetNeighbors<V>"
                }
            ],
            "returns": [
                {
                    "desc": "",
                    "lua_type": "{ V }\n"
                }
            ],
            "function_type": "static",
            "tags": [
                "directed",
                "undirected"
            ],
            "source": {
                "line": 61,
                "path": "lib/graphutil/src/BFS.luau"
            }
        },
        {
            "name": "toSet",
            "desc": "Visits every vertex reachable from `start` in breadth-first order and\nreturns a set `{ [V]: true }` of all visited vertices.  Useful when you\nonly need a reachability check rather than the traversal order itself.\n\n```lua\nlocal reachable = BFS.toSet(\"A\", getNeighbors)\nprint(reachable[\"D\"]) -- true\nprint(reachable[\"Z\"]) -- nil (unreachable)\n```\n\n*Time complexity:* `O(V + E)`",
            "params": [
                {
                    "name": "start",
                    "desc": "",
                    "lua_type": "V"
                },
                {
                    "name": "getNeighbors",
                    "desc": "",
                    "lua_type": "GetNeighbors<V>"
                }
            ],
            "returns": [
                {
                    "desc": "",
                    "lua_type": "{ [V]: boolean }\n"
                }
            ],
            "function_type": "static",
            "tags": [
                "directed",
                "undirected"
            ],
            "source": {
                "line": 100,
                "path": "lib/graphutil/src/BFS.luau"
            }
        },
        {
            "name": "toIterator",
            "desc": "Returns a lazy iterator that yields vertices one at a time in\nbreadth-first order.  The iterator drives the BFS internally via a\nclosure — no coroutines are used.\n\nAliased as [`BFS.forEach`](#forEach).\n\n```lua\nlocal next = BFS.toIterator(\"A\", getNeighbors)\nprint(next()) -- \"A\"\nprint(next()) -- \"B\"\nprint(next()) -- \"C\"\n-- ...\nprint(next()) -- nil  (exhausted)\n```\n\n:::tip for-loop usage\nThe iterator is compatible with a plain `for` loop since Luau supports\n`for x in fn do` for `() -> T?` iterators:\n```lua\nfor vertex in BFS.toIterator(\"A\", getNeighbors) do\n    print(vertex)\nend\n```\n:::\n\n*Time complexity per call:* amortised `O(degree(V))`",
            "params": [
                {
                    "name": "start",
                    "desc": "",
                    "lua_type": "V"
                },
                {
                    "name": "getNeighbors",
                    "desc": "",
                    "lua_type": "GetNeighbors<V>"
                }
            ],
            "returns": [
                {
                    "desc": "",
                    "lua_type": "KIterator<V>\n"
                }
            ],
            "function_type": "static",
            "tags": [
                "directed",
                "undirected"
            ],
            "source": {
                "line": 152,
                "path": "lib/graphutil/src/BFS.luau"
            }
        },
        {
            "name": "forEach",
            "desc": "Alias of [`BFS.toIterator`](#toIterator).",
            "params": [
                {
                    "name": "start",
                    "desc": "",
                    "lua_type": "V"
                },
                {
                    "name": "getNeighbors",
                    "desc": "",
                    "lua_type": "GetNeighbors<V>"
                }
            ],
            "returns": [
                {
                    "desc": "",
                    "lua_type": "KIterator<V>"
                }
            ],
            "function_type": "static",
            "tags": [
                "directed",
                "undirected"
            ],
            "source": {
                "line": 187,
                "path": "lib/graphutil/src/BFS.luau"
            }
        },
        {
            "name": "shortestPath",
            "desc": "Finds the **shortest path** (fewest hops / edges) from `start` to `goal`\nusing BFS.  Returns a [PathResult](/api/GraphUtil#PathResult) where `cost`\nis the hop count, or `nil` if `goal` is unreachable from `start`.\n\nThis is the correct function to use when all edges have equal (or no)\nweight.  For weighted shortest paths use [Dijkstra.shortestPath](/api/Dijkstra#shortestPath).\n\n```lua\nlocal result = BFS.shortestPath(\"A\", \"D\", getNeighbors)\n-- result.path == { \"A\", \"B\", \"D\" }  (or { \"A\", \"C\", \"D\" })\n-- result.cost == 2\n```\n\nPass an optional [ShouldStop](/api/GraphUtil#ShouldStop) predicate to abort a\nlong-running search — checked once per dequeued vertex, receiving that\nvertex's hop-distance from `start` as `cost`. Returning `true` stops the\nsearch and returns `nil`:\n\n```lua\n-- Search no deeper than 6 hops.\nlocal result = BFS.shortestPath(\"A\", \"D\", getNeighbors, function(_vertex, depth)\n    return depth >= 6\nend)\n```\n\n*Time complexity:* `O(V + E)`",
            "params": [
                {
                    "name": "start",
                    "desc": "",
                    "lua_type": "V"
                },
                {
                    "name": "goal",
                    "desc": "",
                    "lua_type": "V"
                },
                {
                    "name": "getNeighbors",
                    "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": 221,
                "path": "lib/graphutil/src/BFS.luau"
            }
        },
        {
            "name": "bidirectionalShortestPath",
            "desc": "Finds the **shortest path** (fewest hops) between `start` and `goal` by\ngrowing a BFS frontier from *both* ends simultaneously and stopping as\nsoon as the two frontiers are provably guaranteed to have met at the\nshortest possible point.  Returns the same [PathResult](/api/GraphUtil#PathResult)\nas [`BFS.shortestPath`](#shortestPath), or `nil` if `goal` is unreachable.\n\nOn large, densely-branching graphs this explores far fewer vertices than\na single-directional search — each side only needs to reach roughly\nhalf the hop-distance, so the total number of vertices visited grows\nwith the *branching factor to the power of half the distance* instead of\nthe full distance.\n\n```lua\nlocal result = BFS.bidirectionalShortestPath(\"A\", \"D\", getNeighbors)\n-- result.path == { \"A\", \"B\", \"D\" }  (or { \"A\", \"C\", \"D\" })\n-- result.cost == 2\n```\n\n:::caution Directed graphs\nGrowing a frontier 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 third callback that returns the *predecessors* of\na vertex (i.e. `getReversedNeighbors(v)` returns every `u` such that\n`u → v` is an edge):\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 = BFS.bidirectionalShortestPath(\"A\", \"D\", getNeighbors, getReversedNeighbors)\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 dequeues a vertex, receiving that vertex's hop-distance\nfrom its own root as `cost`.\n\n*Time complexity:* `O(V + E)` worst case, but typically `O(b^(d/2))` for\na graph with branching factor `b` and hop-distance `d` between `start`\nand `goal`.",
            "params": [
                {
                    "name": "start",
                    "desc": "",
                    "lua_type": "V"
                },
                {
                    "name": "goal",
                    "desc": "",
                    "lua_type": "V"
                },
                {
                    "name": "getNeighbors",
                    "desc": "",
                    "lua_type": "GetNeighbors<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": 332,
                "path": "lib/graphutil/src/BFS.luau"
            }
        },
        {
            "name": "toParentMap",
            "desc": "Performs a full BFS from `start` and returns a **parent map** —\na table where every reachable vertex maps to the vertex it was\ndiscovered from.  The start vertex maps to itself.\n\nThis gives you the BFS spanning tree in one pass, letting you\nreconstruct the shortest hop-path to *any* reachable vertex without\nrunning a separate search per target:\n\n```lua\nlocal function pathTo(parentMap, start, goal)\n    if not parentMap[goal] then return nil end  -- unreachable\n    local path = {}\n    local v = goal\n    while v ~= start do\n        table.insert(path, 1, v)\n        v = parentMap[v]\n    end\n    table.insert(path, 1, start)\n    return path\nend\n\nlocal parents = BFS.toParentMap(\"A\", getNeighbors)\nprint(pathTo(parents, \"A\", \"D\"))  -- { \"A\", \"B\", \"D\" }\nprint(pathTo(parents, \"A\", \"C\"))  -- { \"A\", \"C\" }\n```\n\n*Time complexity:* `O(V + E)`",
            "params": [
                {
                    "name": "start",
                    "desc": "",
                    "lua_type": "V"
                },
                {
                    "name": "getNeighbors",
                    "desc": "",
                    "lua_type": "GetNeighbors<V>"
                }
            ],
            "returns": [
                {
                    "desc": "",
                    "lua_type": "{ [V]: V }\n"
                }
            ],
            "function_type": "static",
            "tags": [
                "directed",
                "undirected"
            ],
            "source": {
                "line": 479,
                "path": "lib/graphutil/src/BFS.luau"
            }
        }
    ],
    "properties": [],
    "types": [],
    "name": "BFS",
    "desc": "Breadth-First Search traversal and unweighted shortest-path algorithms.\n\nBFS explores a graph level-by-level outward from a start vertex, visiting\nall neighbours at the current depth before moving deeper.  This property\nmakes it the correct choice for **unweighted shortest paths** (fewest hops).\nFor weighted shortest paths, use [Dijkstra](/api/Dijkstra) or [AStar](/api/AStar).\n\nAll functions accept either callback form for `GetNeighbors` — see\n[GraphUtil](/api/GraphUtil) for details.\n\n### Example graph\n\n```lua\n--  A ─── B ─── D\n--  │           │\n--  C ──────────┘\n\nlocal adj = {\n    A = {\"B\", \"C\"},\n    B = {\"A\", \"D\"},\n    C = {\"A\", \"D\"},\n    D = {\"B\", \"C\"},\n}\n\nlocal function getNeighbors(v)\n    return adj[v]\nend\n```",
    "source": {
        "line": 36,
        "path": "lib/graphutil/src/BFS.luau"
    }
}