Skip to main content

DFS

Depth-First Search traversal algorithms.

DFS explores a graph by going as deep as possible along each branch before backtracking. It is the basis for cycle detection, topological sorting, and strongly connected component algorithms (see Connectivity and TopologicalSort).

All implementations here are iterative (stack-based) rather than recursive, avoiding stack-overflow issues on large graphs.

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

Example graph

--  A ──→ B ──→ D
--  │
--  ↓
--  C ──→ E

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

local function getNeighbors(v)
    return adj[v]
end
DFS traversal order

Because the iterative stack reverses insertion order relative to visited order, the traversal visits the last neighbour listed first. This differs from recursive DFS which visits the first neighbour first. If a specific neighbour order matters, sort the neighbour list before returning it from getNeighbors.

Functions

toArray

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

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

local visited = DFS.toArray("A", getNeighbors)
-- e.g. { "A", "C", "E", "B", "D" }

Time complexity: O(V + E)

toSet

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

Visits every vertex reachable from start in depth-first order and returns a set { [V]: true } of all visited vertices.

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

Time complexity: O(V + E)

toIterator

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

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

Aliased as DFS.forEach.

local next = DFS.toIterator("A", getNeighbors)
print(next()) -- "A"
print(next()) -- first deep branch vertex
-- ...
print(next()) -- nil  (exhausted)
for-loop usage
for vertex in DFS.toIterator("A", getNeighbors) do
    print(vertex)
end

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

forEach

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

Alias of DFS.toIterator.

toParentMap

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

Performs a full DFS 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.

Useful for reconstructing the DFS spanning tree or testing reachability across all targets in one pass:

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 = DFS.toParentMap("A", getNeighbors)
print(pathTo(parents, "A", "E"))  -- e.g. { "A", "C", "E" }
Not shortest path

DFS parent maps do not guarantee shortest (fewest-hop) paths. Use BFS.toParentMap or BFS.shortestPath when hop-count matters.

Time complexity: O(V + E)

Show raw api
{
    "functions": [
        {
            "name": "toArray",
            "desc": "Visits every vertex reachable from `start` in depth-first order and\nreturns them as an ordered array.\n\n```lua\nlocal visited = DFS.toArray(\"A\", getNeighbors)\n-- e.g. { \"A\", \"C\", \"E\", \"B\", \"D\" }\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": 73,
                "path": "lib/graphutil/src/DFS.luau"
            }
        },
        {
            "name": "toSet",
            "desc": "Visits every vertex reachable from `start` in depth-first order and\nreturns a set `{ [V]: true }` of all visited vertices.\n\n```lua\nlocal reachable = DFS.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": 112,
                "path": "lib/graphutil/src/DFS.luau"
            }
        },
        {
            "name": "toIterator",
            "desc": "Returns a lazy iterator that yields vertices one at a time in\ndepth-first order.  The iterator drives the DFS internally via a\nclosure — no coroutines are used.\n\nAliased as [`DFS.forEach`](#forEach).\n\n```lua\nlocal next = DFS.toIterator(\"A\", getNeighbors)\nprint(next()) -- \"A\"\nprint(next()) -- first deep branch vertex\n-- ...\nprint(next()) -- nil  (exhausted)\n```\n\n:::tip for-loop usage\n```lua\nfor vertex in DFS.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": 162,
                "path": "lib/graphutil/src/DFS.luau"
            }
        },
        {
            "name": "forEach",
            "desc": "Alias of [`DFS.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": 198,
                "path": "lib/graphutil/src/DFS.luau"
            }
        },
        {
            "name": "toParentMap",
            "desc": "Performs a full DFS 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\nUseful for reconstructing the DFS spanning tree or testing\nreachability across all targets in one pass:\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 = DFS.toParentMap(\"A\", getNeighbors)\nprint(pathTo(parents, \"A\", \"E\"))  -- e.g. { \"A\", \"C\", \"E\" }\n```\n\n:::note Not shortest path\nDFS parent maps do **not** guarantee shortest (fewest-hop) paths.\nUse [`BFS.toParentMap`](/api/BFS#toParentMap) or\n[`BFS.shortestPath`](/api/BFS#shortestPath) when hop-count matters.\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": 237,
                "path": "lib/graphutil/src/DFS.luau"
            }
        }
    ],
    "properties": [],
    "types": [],
    "name": "DFS",
    "desc": "Depth-First Search traversal algorithms.\n\nDFS explores a graph by going as deep as possible along each branch before\nbacktracking.  It is the basis for cycle detection, topological sorting,\nand strongly connected component algorithms (see [Connectivity](/api/Connectivity)\nand [TopologicalSort](/api/TopologicalSort)).\n\nAll implementations here are **iterative** (stack-based) rather than\nrecursive, avoiding stack-overflow issues on large graphs.\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--  ↓\n--  C ──→ E\n\nlocal adj = {\n    A = {\"B\", \"C\"},\n    B = {\"D\"},\n    C = {\"E\"},\n    D = {},\n    E = {},\n}\n\nlocal function getNeighbors(v)\n    return adj[v]\nend\n```\n\n:::note DFS traversal order\nBecause the iterative stack reverses insertion order relative to visited\norder, the traversal visits the **last** neighbour listed first.  This\ndiffers from recursive DFS which visits the first neighbour first.\nIf a specific neighbour order matters, sort the neighbour list before\nreturning it from `getNeighbors`.\n:::",
    "source": {
        "line": 50,
        "path": "lib/graphutil/src/DFS.luau"
    }
}