Backtracking

advanced30 min

Choose, recurse, undo

Backtracking is depth-first search over decisions rather than over a graph. You make a choice, explore everything that follows from it, then reverse the choice and try the next one.

Every backtracking method has the same three parts:

StepPurpose
Base caseAll decisions made — record or evaluate the result
Loop over choicesThe options available at this position
Choose, recurse, undoApply a choice, explore, then reverse it

The undo is what makes it correct. Without it, choices leak from one branch into the next.

Generating subsets

static List<Integer> current = new ArrayList<>();
static List<List<Integer>> results = new ArrayList<>();

static void subsets(int[] values, int index) {
    if (index == values.length) {
        results.add(new ArrayList<>(current));   // copy, not the live list
        return;
    }

    // choice 1: skip this element
    subsets(values, index + 1);

    // choice 2: take it
    current.add(values[index]);
    subsets(values, index + 1);
    current.remove(current.size() - 1);          // undo
}

Two lines that are easy to get wrong

new ArrayList<>(current) — storing current directly would put the same list object into every result, and since it keeps changing, every entry would end up identical (and empty at the end).

current.remove(current.size() - 1) — every add before a recursive call needs a matching removal after it. Forget it and current grows without bound, contaminating every later branch.

The symptom of a missing undo is output that is correct for the first branch and wrong afterwards.

Permutations

Here the state to undo is two things — the list and the used-markers.

static void permute(int[] values, boolean[] used) {
    if (current.size() == values.length) {
        results.add(new ArrayList<>(current));
        return;
    }

    for (int i = 0; i < values.length; i++) {
        if (used[i]) continue;

        used[i] = true;                  // choose
        current.add(values[i]);

        permute(values, used);           // recurse

        current.remove(current.size() - 1);
        used[i] = false;                 // undo both
    }
}

Both mutations must be reversed. Undoing only one leaves the search in a corrupt state that is hard to diagnose.

Permutations grow as n factorial — 10 items is about 3.6 million arrangements, 13 is over 6 billion. Check the size before choosing this approach.

Choosing k of n

Passing a start index keeps combinations in increasing order, so each is produced once rather than in every possible ordering:

static void choose(int[] values, int start, int k) {
    if (current.size() == k) {
        results.add(new ArrayList<>(current));
        return;
    }

    for (int i = start; i < values.length; i++) {
        current.add(values[i]);
        choose(values, i + 1, k);        // i + 1, not start + 1
        current.remove(current.size() - 1);
    }
}

Passing i + 1 rather than start + 1 is the detail. It advances past the element just used, which is what prevents duplicates.

Pruning

The search space grows exponentially, so abandoning hopeless branches early is often what makes a solution feasible.

static boolean canReachTarget(int[] values, int index, int sum, int target) {
    if (sum == target) return true;
    if (sum > target) return false;                  // prune: already too big
    if (index == values.length) return false;

    // take it, or skip it
    return canReachTarget(values, index + 1, sum + values[index], target)
        || canReachTarget(values, index + 1, sum, target);
}

The sum > target check cuts off entire subtrees. Assuming all values are positive, adding more can never bring an overshooting sum back down, so nothing below that branch can succeed.

A prune must actually be sound

Pruning is only valid if the condition genuinely rules out every completion of that branch. The check above relies on values being positive — with negatives, an overshooting sum could come back down, and the prune would discard real answers.

A prune that is merely usually-right produces wrong answers on the cases you did not think about. Be able to state why nothing below the branch can succeed.

Carrying sum as a parameter rather than recomputing it at the base case is what makes the prune possible at all — you need the running value at every level, not just the end.

A worked example: placing rooks

Place n rooks on an n × n board so none share a row or column.

static int countArrangements(int n, int row, boolean[] usedColumns) {
    if (row == n) return 1;              // placed them all

    int total = 0;
    for (int col = 0; col < n; col++) {
        if (usedColumns[col]) continue;

        usedColumns[col] = true;         // choose
        total += countArrangements(n, row + 1, usedColumns);
        usedColumns[col] = false;        // undo
    }
    return total;
}

Handling one row per call means rows can never clash, so only columns need tracking. Choosing the right thing to iterate over frequently removes half the constraints.

Recognising a backtracking problem

Signals

  • The problem asks for all arrangements, combinations, or paths
  • It asks whether some arrangement is possible
  • The input bound is small — roughly n ≤ 20 for subsets, n ≤ 10 for permutations
  • Choices are made in sequence and each constrains the next

If the bound is large, backtracking will not finish, and the intended solution is probably dynamic programming or something greedy.

Common mistakes

  • Missing the undo, leaking state between branches.
  • Storing the live list instead of a copy.
  • Undoing only part of the state when several things changed.
  • start + 1 instead of i + 1 in combinations, producing duplicates.
  • An unsound prune that discards valid answers.
  • No prune at all where one is available, making the search far slower than necessary.
  • Ignoring the input bound and writing an exponential search for a large input.

Practice

  1. Print every subset of an array of five integers.
  2. Print every permutation of a four-character string.
  3. Print every combination of exactly 3 elements chosen from 5.
  4. Determine whether any subset of an array sums to a target, with pruning.
  5. Count the ways to place n non-attacking rooks on an n × n board, for n up to 8.
Hints
  1. Skip-or-take at each index.
  2. A used array; undo both mutations.
  3. Pass i + 1 as the next start.
  4. Carry the running sum as a parameter so you can prune on it.
  5. One row per recursive call; only track columns.

Next

Related