Minimum Jumps to Reach Home — State-Space BFS with Direction Memory

Sanjeev SharmaSanjeev Sharma
7 min read

Advertisement

Problem Statement

A bug starts at position 0 on the number line and wants to reach x. On each move it can jump forward by a units or jump backward by b units, with two restrictions: it cannot jump to negative positions, and it cannot jump backward two times in a row. You are also given a list of forbidden positions where the bug must never land. Return the minimum number of jumps required to reach x, or -1 if impossible.

This is a shortest-path problem on an implicit graph whose state must include both the current position and a flag for whether the previous move was backward.

Why This Problem Matters

Minimum Jumps to Reach Home tests whether you can recognize that a seemingly simple number-line puzzle is actually a state-space BFS where the state is more than just position. Many candidates default to position-only BFS and either get the wrong answer (because they incorrectly forbid valid backward jumps) or time out (because they revisit positions through different direction states).

It also requires deriving a non-trivial upper bound on positions to keep BFS finite. The key reasoning is that if x is reachable, the bug never needs to go past x plus a plus b (or some similar bound), and if it never reaches x, BFS will terminate when the queue empties. Articulating that bound to the interviewer is what separates strong candidates from average ones.

The Core Insight

The state is the pair (position, justJumpedBackward). Two states with the same position but different direction flags are distinct because the legal moves differ. From (pos, False) you may jump forward to (pos + a, False) or backward to (pos - b, True). From (pos, True) you may jump forward but you cannot jump backward again. Visited tracking must therefore key on the full state, not just position.

The upper bound on positions deserves attention. Without a cap, BFS could explore infinitely along the positive number line. A safe bound is max(x, max(forbidden)) + a + b. Beyond this bound, any further forward progress is wasted because you would only need to come back, and forbidden positions cannot extend past the cap. Some references prove the tighter bound 2000 + 2 * a + b, but the bound here is correct and easier to derive in an interview.

Visual Dry Run (BFS/DFS trace)

Take forbidden equal to [14, 4, 18, 1, 15], a equal to 3, b equal to 15, x equal to 9.

Compute limit equal to max(9, 18) + 3 + 15 equal to 36. Mark (0, False) visited and push it with steps 0.

Pop (0, False, 0). 0 not equal to x. Forward: 3 not forbidden, not visited, push (3, False, 1). Backward: 0 minus 15 is negative, skip.

Pop (3, False, 1). 3 not equal to x. Forward: 6, push (6, False, 2). Backward: 3 minus 15 negative, skip.

Pop (6, False, 2). Forward: 9, equals x, return 2 jumps.

Wait, that returns 2 but the actual LeetCode answer for this example is 3. Let me retrace; the test order is forbidden first. Forward 3 is fine (not in forbidden set {14, 4, 18, 1, 15}), push. Forward 6 is fine, push. Forward 9 is fine and equals target. Return 2. The textbook answer for these inputs is 3, but the textbook problem uses [14, 4, 18, 1, 15], a equal 3, b equal 15, x equal 9 with a different specification. Strictly per LeetCode rules, 0 to 3 to 6 to 9 takes 3 forward jumps. The trace above pushed (3, False, 1) and (6, False, 2) and then on pop checks if 6 equals x first, no, then forward to 9 with steps 3 in the push, return 3. The fix in the dry run is: when pushing (npos, False, steps + 1) we tag the eventual answer with steps + 1. So pop (6, False, 2), push (9, False, 3). On the next pop, position equals x, return 3. The correct answer is 3 jumps.

Solution (Optimal)

Python

from collections import deque
 
class Solution:
    def minimumJumps(self, forbidden, a, b, x):
        bad = set(forbidden)
        limit = max(x, max(forbidden) if forbidden else 0) + a + b
        visited = {(0, False)}
        q = deque([(0, False, 0)])
        while q:
            pos, back, steps = q.popleft()
            if pos == x:
                return steps
            npos = pos + a
            if npos <= limit and npos not in bad and (npos, False) not in visited:
                visited.add((npos, False))
                q.append((npos, False, steps + 1))
            if not back:
                npos = pos - b
                if npos >= 0 and npos not in bad and (npos, True) not in visited:
                    visited.add((npos, True))
                    q.append((npos, True, steps + 1))
        return -1

JavaScript

var minimumJumps = function(forbidden, a, b, x) {
    const bad = new Set(forbidden);
    const limit = Math.max(x, ...forbidden, 0) + a + b;
    const visited = new Set();
    visited.add('0,0');
    const q = [[0, 0, 0]];
    while (q.length) {
        const [pos, back, steps] = q.shift();
        if (pos === x) return steps;
        const fwd = pos + a;
        const fwdKey = fwd + ',0';
        if (fwd <= limit && !bad.has(fwd) && !visited.has(fwdKey)) {
            visited.add(fwdKey);
            q.push([fwd, 0, steps + 1]);
        }
        if (!back) {
            const bwd = pos - b;
            const bwdKey = bwd + ',1';
            if (bwd >= 0 && !bad.has(bwd) && !visited.has(bwdKey)) {
                visited.add(bwdKey);
                q.push([bwd, 1, steps + 1]);
            }
        }
    }
    return -1;
};

Time complexity is O(limit) because each (position, direction) state is visited at most once. Space complexity is O(limit) for the visited set and queue.

Common Mistakes

Tracking visited by position only causes the BFS to skip valid paths that re-enter a position with a different direction flag. Not deriving an explicit upper bound and letting BFS expand forever causes time-limit-exceeded on the largest test cases. Forgetting the constraint that you cannot jump backward twice in a row leads to wrong answers like 1 when the correct answer is 2 or more. Forgetting the negative-position guard either crashes or produces incorrect paths. Treating forbidden as a list rather than a set turns each lookup into O(F), which compounded with BFS makes the algorithm quadratic.

Interview Tips

Lead with the realization that the state must encode direction memory. Walk through one example showing why position-only state is insufficient. Justify the upper bound aloud; even a hand-wavy derivation earns major credit. Use a hash set for forbidden positions so lookups are constant time. Acknowledge the BFS termination case (queue empty without reaching x) and return -1 outside the loop, never inside. If the interviewer pushes for tighter bounds, mention the published proof that 2000 plus a plus b suffices, but emphasize that your bound is correct and conservative.

Follow-up Questions

What if the bug can jump forward by any of multiple values? Add edges for each forward jump and rerun BFS; the state remains (position, justJumpedBackward). What if there is a cooldown where after a backward jump you must wait two forward jumps before another backward? Extend the state to include a count or last-direction history. How would you find all shortest paths? Add a parents map keyed by state and reconstruct after BFS. What if positions are continuous instead of integer? You enter shortest-path on a metric space, which BFS no longer solves; switch to Dijkstra with a small step heuristic.

Key Takeaways

  • The state must be (position, justJumpedBackward) because legal moves depend on direction history
  • BFS on this state space yields the minimum number of jumps
  • Derive an explicit upper bound on position so BFS terminates; max(x, max(forbidden)) + a + b is safe
  • Use a hash set for forbidden to keep lookups constant time
  • Always check the no-negative-position constraint and the backward-twice constraint
  • Pattern unlocks Open the Lock, Sliding Puzzle, and any direction-aware shortest-path problem

Advertisement

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro

Related reading