Recursion & Backtracking · Recursion & Backtracking Fundamentals

Backtracking: Subsets & Choices

The include/exclude recursive branching pattern behind subsets, permutations, and combination problems.

  • 5 min read
  • 11 practice questions
  • 7 graded problems
On this page
  1. Description
  2. The approach
  3. A second flavor: duplicates in the input
  4. Where you'll see it
  5. Which tool for which job?
  6. Common mistakes
  7. Check yourself
  8. Key takeaways
  9. Quick revise
  10. Practice

Description

"All subsets", "all combinations that add up to a target" and "how many subsets hit this sum" all ask about every way of choosing from a set, and there are 2^n of them. You can't write the nested loops ahead of time for an n you don't know. Backtracking makes the choosing systematic. Recursion walks a tree of decisions, one element at a time: choose, explore, then undo the choice. A single shared list holds the current path.

We'll go from simple to more technical:

  1. The pattern: choose, explore, un-choose, and record a copy.
  2. A second flavor: duplicates in the input, skipped with i > start.
  3. Where enumeration shows up in production (configurations, test cases, search).
  4. What breaks it: shared lists, missing un-choose, over-eager deduplication, no pruning.

The approach

Java

// Decide in/out for each element: a binary tree of choices
int countSubsetsWithSum(int[] nums, int i, int remaining) {
    if (i == nums.length) return remaining == 0 ? 1 : 0;              // one complete subset
    return countSubsetsWithSum(nums, i + 1, remaining - nums[i])       // include nums[i]
         + countSubsetsWithSum(nums, i + 1, remaining);                // exclude nums[i]
}

// Build the subsets themselves: choose, explore, un-choose
void subsets(int[] nums, int start, List<Integer> current, List<List<Integer>> result) {
    result.add(new ArrayList<>(current));          // record a COPY - current keeps changing
    for (int i = start; i < nums.length; i++) {
        current.add(nums[i]);                      // choose
        subsets(nums, i + 1, current, result);     // explore: only later elements, so no repeats
        current.remove(current.size() - 1);        // un-choose before trying the next element
    }
}

Python

# Decide in/out for each element: a binary tree of choices
def count_subsets_with_sum(nums: list[int], i: int, remaining: int) -> int:
    if i == len(nums):
        return 1 if remaining == 0 else 0                          # one complete subset
    return (count_subsets_with_sum(nums, i + 1, remaining - nums[i])   # include nums[i]
            + count_subsets_with_sum(nums, i + 1, remaining))          # exclude nums[i]


# Build the subsets themselves: choose, explore, un-choose
def subsets(nums: list[int], start: int, current: list[int], result: list[list[int]]) -> None:
    result.append(current[:])                  # record a COPY - current keeps changing
    for i in range(start, len(nums)):
        current.append(nums[i])                # choose
        subsets(nums, i + 1, current, result)  # explore: only later elements, so no repeats
        current.pop()                          # un-choose before trying the next element

Both shapes enumerate the same 2^n subsets. The loop version grows each subset by picking the next element from start onward, which keeps elements in index order and never produces the same subset twice. Every recursion needs its base case - here, running out of elements - or it never stops. Combination Sum is the same loop with one change: recursing with i instead of i + 1 lets the same element be chosen again.

A second flavor: duplicates in the input

With repeated values, two different index choices can build the same subset ([1, 2a] and [1, 2b]). Sort the input, then at each loop level skip a value equal to the one just tried at that level:

Java

Arrays.sort(nums);
void subsetsWithDup(int[] nums, int start, List<Integer> current, List<List<Integer>> result) {
    result.add(new ArrayList<>(current));
    for (int i = start; i < nums.length; i++) {
        if (i > start && nums[i] == nums[i - 1]) continue;   // same value, same level: already covered
        current.add(nums[i]);
        subsetsWithDup(nums, i + 1, current, result);
        current.remove(current.size() - 1);
    }
}

Python

def subsets_with_dup(nums: list[int], start: int, current: list[int], result: list[list[int]]) -> None:
    result.append(current[:])
    for i in range(start, len(nums)):
        if i > start and nums[i] == nums[i - 1]:
            continue                           # same value, same level: already covered
        current.append(nums[i])
        subsets_with_dup(nums, i + 1, current, result)
        current.pop()

# call it on sorted input: nums.sort()

i > start is the precise rule: the first copy of a value at a level is always allowed, so [2, 2] can still be built by taking the first 2 at one level and the second 2 one level deeper. Writing i > 0 also blocks that second 2 and loses [2, 2] and [1, 2, 2].

Where you'll see it

Test-matrix generation (which combinations of feature flags or configuration options to exercise), bundle and portfolio selection under a budget, and puzzle and scheduling solvers are backtracking over choices. Real systems keep them tractable with pruning, symmetry breaking and memoization, because the raw space doubles with every option.

Which tool for which job?

You needUse
Every subset or combinationbacktracking: choose, explore, un-choose
Input with repeated valuessort, then skip i > start && nums[i] == nums[i - 1]
Only the number of subsets with a sumDP over sums, not enumeration
Subset sum with n around 40meet in the middle: two halves of 2^20
Every subset, one change at a timeGray code order
Results too many to hold in memorya generator that streams them

Common mistakes

MistakeWhat happensDo this instead
Recording current itselfeight empty lists for [1, 2, 3]record a copy
Forgetting to un-chooseearlier choices leak into later resultsundo after each recursive call
Deduplicating with i > 0loses [2, 2] and [1, 2, 2]i > start
continue instead of break on sorted candidates132,719 checks instead of 41,530break once a candidate is too big
Enumerating to count8,388,607 calls where DP needed 2,898 updatesDP when only the count matters

Check yourself

Try to answer before opening each one - recalling beats re-reading.

1. A subsets generator records results with result.add(current). What comes back for [1, 2, 3]?

Show answer

Eight empty lists. Every entry refers to the same current list, which ends empty after the last un-choose. Recording new ArrayList<>(current) freezes each subset at the moment it's complete.

2. Why is the duplicate check i > start rather than i > 0?

Show answer

Only the second copy of a value at the same loop level repeats work. With i > start, the first copy at each level is allowed, so [2, 2] is built by using one 2 per level. i > 0 also skips the second 2 one level deeper and loses [2, 2] and [1, 2, 2].

3. What single change turns "combinations using each element at most once" into Combination Sum (unlimited reuse)?

Show answer

Recurse with i instead of i + 1: the next choice may start at the element just used, so it can be picked again. Starting at i (not 0) still keeps the chosen values in index order, so each combination is produced once.

Key takeaways

  • Backtracking walks a tree of choices with one shared working list: choose, explore, un-choose.
  • Record a copy of the working list, never the list itself.
  • Include/exclude gives 2^n subsets; the start-index loop enumerates them without repeats.
  • With duplicate values: sort, then skip equal siblings with i > start.
  • Prune as soon as a branch can't succeed; switch to DP when only a count is needed.

Quick revise

The whole skill on one page, for the night before an interview.

This page puts the lesson and the deep dive together. Skim it before practice or an interview, and go back to the full lesson for anything you don't recognize.

Pick the tool

You needUse
Every subsetchoose, explore, un-choose
Repeated valuessort plus i > start skip
A countDP over sums
n around 40, big valuesmeet in the middle
One change per stepGray code
Huge outputstream with a generator

Snippets worth knowing by heart

Java

void backtrack(int[] nums, int start, List<Integer> cur, List<List<Integer>> out) {
    out.add(new ArrayList<>(cur));                    // a copy
    for (int i = start; i < nums.length; i++) {
        if (i > start && nums[i] == nums[i - 1]) continue;
        cur.add(nums[i]);
        backtrack(nums, i + 1, cur, out);
        cur.remove(cur.size() - 1);                   // un-choose
    }
}

Python

ways = [1] + [0] * target               # count subsets by sum, no enumeration
for x in nums:
    for s in range(target, x - 1, -1):
        ways[s] += ways[s - x]

The traps

  • Recording the shared list: every result ends up as the same, finally empty, list. Record a copy.
  • No un-choose: earlier choices leak into later results. Undo the choice after each recursive call.
  • i > 0 in place of i > start: it drops valid results like [2, 2]. Skip duplicates only within one level.
  • continue where break would prune: sorted candidates past the target are still tried. break once one is too big.
  • Enumerating to count: listing every subset takes millions of calls. Use DP when only the count matters.

Staff-level points, one line each

  • Combination Sum to 60: break checked 41,530 candidates, continue 132,719. Both found 1,966 combinations.
  • Counting subsets to 150 among 22 numbers: 8,388,607 calls by enumeration, 2,898 DP updates.
  • Meet in the middle: 2 × 1,048,576 half-sums instead of 2^40 subsets.
  • Gray code: all 16 subset sums from 15 single updates.
  • Streaming 18-item subsets used under 1/100 of the memory of storing them.

Say it in an interview

  • "Choose, explore, un-choose, and I record a copy at each node."
  • "With duplicates I sort and skip a value equal to the previous one at the same level."
  • "If only the count matters and sums are small, I'd switch to DP."
  • "For n around 40, I'd meet in the middle."

Practice

This skill comes with 11 multiple-choice questions that tell you which misconception a wrong answer reveals and 7 graded problems. Your answers are graded and your mastery is tracked, so you'll know when the skill holds up. Running code and saving progress need a free account.

Practice Backtracking: Subsets & Choices free

Count Subsets With a Given Sum

MediumProCoding problemJava · Python

Given an array of integers nums and an integer target, count how many subsets of nums (chosen by index — two elements with equal value but different positions count as distinct choices) sum exactly to target.

The full statement, the examples and the hidden test suite are part of AI Proof Engineers Pro.

Subset Sum (All Subset Sums)

MediumProCoding problemJava · Python

Given an array nums, return the sum of every one of its 2^n subsets (including the empty subset, which sums to 0), sorted ascending. Duplicate sums are kept, not deduplicated.

The full statement, the examples and the hidden test suite are part of AI Proof Engineers Pro.

Combination Sum

MediumProCoding problemJava · Python

From a list of distinct positive integers candidates, find every way to pick numbers (repeats allowed, as many times as you like) that add up to target. Write each combination in ascending order, and sort the combinations by comparing them element by element.

The full statement, the examples and the hidden test suite are part of AI Proof Engineers Pro.

Combination Sum II

MediumProCoding problemJava · Python

candidates may contain the same number more than once. Find every combination of its entries that adds up to target, using each entry at most once. Two combinations made of the same values count as one, so list each only once.

The full statement, the examples and the hidden test suite are part of AI Proof Engineers Pro.

Subsets II

MediumProCoding problemJava · Python

nums may hold repeated values. List every distinct subset of it, the empty one included, with each subset's values in ascending order and the subsets sorted by comparing them element by element.

The full statement, the examples and the hidden test suite are part of AI Proof Engineers Pro.

Power Set

MediumProCoding problemJava · Python

List every subset of nums, a list of distinct integers, including the empty subset. Write each subset's values in ascending order, and sort the subsets by comparing them element by element.

The full statement, the examples and the hidden test suite are part of AI Proof Engineers Pro.

N-Queens: Count Distinct Solutions

HardProCoding problemJava · Python

Place n chess queens on an n x n board so that no two of them share a row, a column or a diagonal. Count how many different ways there are to do it. Return the count, not the boards.

The full statement, the examples and the hidden test suite are part of AI Proof Engineers Pro.

Practice Backtracking: Subsets & Choices free

No card needed. Lessons, practice questions and easy problems are free forever.

Free plan, no card · 7-day Pro trial · Cancel anytime