Depth-Limited Search vs Iterative Deepening DFS: The Real Tradeoff

June 10, 202610 min read
dsaalgorithmsinterview-prepgraphs
Depth-Limited Search vs Iterative Deepening DFS: The Real Tradeoff
TL;DR
  • Depth-limited search (DLS) returns three states: solution, failure, or cutoff — cutoff means "I stopped looking," not "goal doesn't exist."
  • Iterative deepening DFS (IDDFS) runs DLS from depth 0 upward, pairing DFS memory efficiency with BFS completeness and optimality.
  • Re-exploration overhead is bounded by b/(b-1) — at branching factor 10 that's only 11% extra work, because the deepest level always dominates.
  • Use DLS when you know the solution depth (chess engine horizons); use IDDFS when the depth is unknown and memory is constrained.
  • IDA* extends IDDFS with an admissible heuristic cost threshold, making it optimal for weighted state spaces like Rubik's cube.
  • In interviews, IDDFS beats BFS when memory is a stated constraint and you still need the shortest-path guarantee.

IQ bell curve meme: low IQ and high IQ both ignore memory management while mid IQ panics over every byte

You want shortest-path guarantees. BFS delivers. So you implement BFS, watch it chew through RAM, and realize a tree with branching factor 10 and solution depth 10 wants you to hold 10 billion nodes in memory. Your laptop fans start to sound like a jet engine. Not happening.

You switch to DFS. Linear space. Problem solved. Until DFS cheerfully descends into an infinite branch while the goal sits five hops to the right, and your program runs until the heat death of the universe. Also not great. The BFS vs DFS tradeoff is exactly this tension: completeness and optimality cost memory.

Depth-limited search and iterative deepening DFS are both variants that put a ceiling on the infinite-descent problem. They solve it differently, and knowing which to use requires understanding what each is actually doing.

The Enemy Is Infinite Depth

Pure DFS has no concept of "too deep." In a finite tree, that's fine. In an infinite graph, or any tree where DFS picks the wrong branch first, it runs forever. DFS has no opinion about this. It will spiral down an infinite path with complete serenity while you watch your cursor blink.

The fix is a hard ceiling on recursion depth. Depth-limited search (DLS) stops at a depth limit; iterative deepening DFS (IDDFS) applies that ceiling in a loop, growing it from zero upward.

Depth-Limited Search: Cutting Off at a Known Horizon

DLS is DFS with a hard stop at depth limit. Reach the limit without finding the goal, and you return a signal instead of continuing.

Here's the part that trips people up: DLS has three return states, not two. Standard DFS says found or not-found. DLS says:

  • solution: goal found within the depth limit
  • failure: every node within the limit was explored, goal definitely doesn't exist
  • cutoff: hit the limit, but can't say whether the goal exists deeper

That cutoff state is load-bearing. "Failure" means the solution isn't there. "Cutoff" means you stopped looking. Collapsing those two into a single "not found" is how you write a bug that's invisible until your search terminates incorrectly on every infinite-depth problem you'll ever touch.

def depth_limited_search(node, goal, limit): if node == goal: return node if limit == 0: return "cutoff" cutoff_occurred = False for child in expand(node): result = depth_limited_search(child, goal, limit - 1) if result == "cutoff": cutoff_occurred = True elif result != "failure": return result return "cutoff" if cutoff_occurred else "failure"
type SearchResult<T> = T | "cutoff" | "failure"; function depthLimitedSearch<T>( node: T, goal: T, expand: (node: T) => T[], limit: number ): SearchResult<T> { if (node === goal) return node; if (limit === 0) return "cutoff"; let cutoffOccurred = false; for (const child of expand(node)) { const result = depthLimitedSearch(child, goal, expand, limit - 1); if (result === "cutoff") { cutoffOccurred = true; } else if (result !== "failure") { return result; } } return cutoffOccurred ? "cutoff" : "failure"; }

Time complexity is O(b^l) where b is branching factor and l is the depth limit. Space is O(b·l), linear in the limit. DLS keeps a DFS path on the stack, not the entire frontier. For a deeper look at how recursive call stacks consume space, see recursion space complexity.

When DLS makes sense

DLS works when you have prior knowledge about the depth of a solution. Chess engines are the classic example: you set a search horizon (say, 6 moves ahead) and treat anything beyond that as a leaf node with an evaluation score. You know you want exactly 6-ply search. DLS is the right tool.

If you don't know the depth, DLS breaks down. Set the limit too low, you miss the solution and get "cutoff." Set it too high, you waste time on deep useless branches. You need a way to search without knowing the depth in advance.

Iterative Deepening DFS: Repeating the Work That Doesn't Actually Cost Much

IDDFS runs DLS in a loop, starting at depth 0 and incrementing until it finds the goal:

def iddfs(root, goal): for limit in range(0, float("inf")): result = depth_limited_search(root, goal, limit) if result != "cutoff": return result # either solution or failure
function iddfs<T>(root: T, goal: T, expand: (node: T) => T[]): SearchResult<T> { for (let limit = 0; ; limit++) { const result = depthLimitedSearch(root, goal, expand, limit); if (result !== "cutoff") return result; } }

The obvious objection: you're re-exploring the tree from scratch every iteration. Exponentially wasted work.

It looks like a crime. The math says it's a misdemeanor at worst.

The Overhead Is Smaller Than You Think

Let b be the branching factor and d be the depth of the shallowest solution. IDDFS does the following node expansions:

Depth 0 pass:  1 node           (the root)
Depth 1 pass:  1 + b nodes
Depth 2 pass:  1 + b + b²
...
Depth d pass:  1 + b + b² + ... + b^d  (the full last pass)

Total nodes expanded across all passes:

(d+1)·b⁰ + d·b¹ + (d-1)·b² + ... + 1·b^d

This simplifies to approximately b^d · b/(b-1).

For large branching factors that overhead is tiny:

Branching factor bIDDFS overhead vs single DLS pass at depth d
22x (doubles the work)
31.5x
51.25x
101.11x (only 11% extra)
1001.01x

The deepest level contains b^d nodes, exponentially larger than all the nodes above it combined. Repeating the shallower work doesn't add up to much. The last pass dominates.

4-panel comic: "I hate myself" staring at blank screen, then opens laptop, "Wow! I hate this more"

Your first reaction to IDDFS re-traversing the whole tree on every pass. Then you check the branching factor.

Running Both on the Same Tree

          A
        / | \
       B  C  D
      /|     |
     E  F    G
    /
   H

Branching factor roughly 3, goal is H at depth 3.

DLS with limit=2: Explores A, B, E, F, C, D, G. Hits limit on E (can't descend to H). Returns "cutoff." You don't find H.

DLS with limit=3: Explores A, B, E, H (found!). Returns H. You needed to know limit=3 in advance.

IDDFS:

  • Depth 0: Visits A. Cutoff.
  • Depth 1: Visits A, B, C, D. Cutoff.
  • Depth 2: Visits A, B, E, F, C, D, G. Cutoff.
  • Depth 3: Visits A, B, E, H. Found!

Total nodes: 1 + 4 + 7 + 4 = 16. DLS at depth 3 alone would have explored the same last pass (4 nodes from A down the left subtree). The overhead from earlier passes is 12 nodes. Those 12 nodes become invisible once the tree gets deep enough.

The Four Properties That Matter

PropertyDFSDLSIDDFS
CompleteNo (infinite graphs)No (if limit < solution depth)Yes
Optimal (uniform cost)NoNoYes
TimeO(b^m)O(b^l)O(b^d)
SpaceO(bm)O(bl)O(bd)
Requires knowing depthNoYesNo

IDDFS combines the best of both worlds: BFS-level completeness and optimality, DFS-level space usage. It's the standard recommended algorithm when you need completeness but can't afford BFS memory.

IDA*: When Depth Alone Isn't Enough

IDDFS has a well-known variant that brings A* heuristics into the picture. IDA* (iterative deepening A*) runs IDDFS but instead of using depth as the cutoff, it uses f(n) = g(n) + h(n), where g is the path cost so far and h is an admissible heuristic.

At each iteration, you raise the cutoff to the minimum f-value that exceeded the previous cutoff. The result is an algorithm that finds optimal solutions under non-uniform costs, uses O(d) space (not the exponential table that memoized A* would need), and is how optimal Rubik's cube solvers work in practice.

The core loop looks identical to IDDFS. The difference is entirely in the cutoff condition inside DLS.

When Each One Wins

Use DLS when:

  • You know the exact depth or have a hard horizon constraint (chess engine with time budget)
  • You're doing iterative deepening yourself and need the primitive
  • You want to prune a search tree to a fixed depth for evaluation purposes

Use IDDFS when:

  • You don't know the solution depth
  • Memory is constrained (can't hold BFS frontier)
  • You need the shallowest solution to be guaranteed optimal
  • The branching factor is large (overhead is negligible at b >= 5)

Avoid both when:

  • Edge weights matter and paths have different costs (use Dijkstra or A* or IDA*)
  • You're in a dense graph where DFS with a visited set would do just as well
  • The solution is always at a fixed depth (just use BFS at that level, or DLS directly)

Where This Shows Up in Interviews

These algorithms come up in a specific class of problems: state-space search, tree problems with unknown depth, and any scenario where you need to find a shortest path without paying BFS memory costs.

In a coding interview, the IDDFS pattern is valuable when an interviewer gives you an implicit graph (word ladder, minimum mutations, sliding puzzle) and asks you to minimize space. The instinct is BFS. The better answer, if memory is a stated constraint, is IDDFS. If you're already comfortable with converting recursive DFS to iterative form, IDDFS is a natural next step.

The bigger trap is forgetting that DLS returns three values. If your DLS only returns "found" or "not found," you lose the ability to distinguish "definitely not here" from "I stopped looking too early." That distinction is what makes IDDFS termination correct. Getting this wrong is also a great way to turn a 15-minute implementation into a 40-minute debugging session while your interviewer looks increasingly concerned.

Interviewer asks candidate to sort 0s, 1s, and 2s. Candidate starts with bubble sort. Interviewer: "My grandma runs faster than your code."

Returning "not found" when you meant "cutoff" has this same energy.

If you want to practice reasoning through these tradeoffs out loud under interview conditions, SpaceComplexity runs voice-based DSA mock interviews where this kind of algorithm analysis is exactly what gets evaluated.

The Short Version

  • DLS is DFS with a hard depth ceiling. Three return states: solution, failure, cutoff. Use it when the depth is known.
  • IDDFS is DLS in a loop. Runs from depth 0 upward. Complete, optimal for unit costs, linear space.
  • The overhead from re-exploration is bounded by b/(b-1). At branching factor 10, that's 11%. The last level always dominates.
  • IDDFS is the standard choice when you need BFS guarantees but can't afford BFS memory.
  • IDA is IDDFS with a heuristic cost threshold.* Used for optimal solutions in weighted state spaces like Rubik's cube.

Further Reading