Informed search

Iterative-Deepening A* (IDA*)

DFS on a leash: raise the cost limit until the goal appears.

Interactive demo coming soon β€” read the theory below

The idea

IDA* is A*'s memory-light cousin. It runs plain depth-first search, but with a rule: never go past a "budget" measured by f = g + h, where g is the cost so far and h is a guess of the cost still to go. Each round it tries again with the smallest budget that was too tight last time. This keeps A*'s smart, goal-directed guidance while using almost no memory.

How it works

  1. Set the starting budget (the threshold) to f(start) = h(start), since g is 0 at the start.
  2. Begin a depth-first search from the start node, tracking g (cost accumulated on the current path).
  3. At each node, compute f = g + h. If f is greater than the current threshold, do NOT expand it β€” instead remember this f as a candidate for the next threshold and back up.
  4. If a node is the goal, stop and return the path β€” this is your answer.
  5. Otherwise keep diving depth-first into children, exactly like DFS, so only the current path sits in memory.
  6. When the whole search finishes without a goal, look at all the f values that exceeded the threshold and take the smallest one.
  7. Raise the threshold to that smallest exceeding f and run the entire depth-first search again from scratch.
  8. Repeat until the goal is found; because the threshold climbs in small steps, the first goal found is the cheapest.

OPEN, CLOSED & the data structures

The recursion stack itself (holding only the current root-to-node path) plus a single number: the current f-threshold. During each round it also tracks the minimum f value seen that exceeded the threshold, which becomes the next round's threshold. Unlike A*, there is no big OPEN priority queue and no CLOSED set of all visited nodes.

Pseudocode

function IDA_STAR(start):
    threshold = h(start)                 # f = g + h, and g(start) = 0
    loop:
        next_threshold = INFINITY
        found = DFS(start, g=0, threshold)   # returns FOUND or the min exceeding f
        if found is a goal path: return found
        if next_threshold == INFINITY: return FAILURE   # no nodes left to try
        threshold = next_threshold           # raise to smallest f that overshot

function DFS(node, g, threshold):
    f = g + h(node)
    if f > threshold:
        next_threshold = min(next_threshold, f)   # remember for next round
        return NOT_FOUND
    if node is goal: return path-to(node)
    for each child of node:
        result = DFS(child, g + cost(node, child), threshold)
        if result is a goal path: return result
    return NOT_FOUND

At a glance

Complete?Complete β€” on finite graphs (or with a finite optimal cost) it will find a goal if one exists, because the threshold keeps rising until the goal's f is included.
Optimal?Optimal when h is admissible (never overestimates the true remaining cost). Since the threshold grows to the smallest exceeding f each round, the first goal reached is guaranteed to be a cheapest-cost one.
TimeO(b^d) in the typical case β€” same big-O as A*, but with extra constant-factor overhead from re-exploring nodes every round. Can degrade badly if each round adds only one new node (many distinct f-values).
SpaceO(b*d) β€” linear, like iterative deepening. It only stores the current path, not the whole frontier.

πŸ‘ When to use it

  • You want A*-quality optimal answers but A* uses too much memory.
  • Puzzles with huge state spaces and admissible heuristics, e.g. the 15-puzzle or Rubik's cube.
  • Edge costs vary (not all steps equal), so a plain depth counter won't do β€” you need f = g + h.
  • Memory is the bottleneck, and re-doing some work each round is an acceptable trade.

⚠️ Watch out for

  • It re-expands the same shallow nodes every round β€” wasteful when costs are many distinct real numbers (each round may add just one node).
  • The threshold must rise to the smallest f that EXCEEDED it, not by a fixed step; guessing a step size breaks optimality.
  • With a non-admissible (overestimating) h, you lose the optimality guarantee.
  • Basic IDA* keeps only the current path, so it can revisit the same state via different paths and won't detect cycles unless you add a path check.
  • It shines with a few discrete cost levels; on graphs with wildly varying costs, prefer A* or a variant like RBFS.