</> MAANG.io
coding interview · 201

Intermediate

Advanced coding interview preparation covering complex algorithms, system design, and optimization techniques for senior-level positions.

0/182 solved 0% complete

Permutations and Combinations

What is this?

Enumerating arrangements is the easiest kind of backtracking to write and the easiest to get subtly wrong. The search itself is mechanical — pick something, recurse, put it back. The difficulty is duplicates: when the input contains repeated values, a naive enumeration emits the same arrangement several times, and filtering them afterwards with a set is both slower and a sign that you did not understand where they came from.

The fix is a single line, and knowing exactly why it works is what the interview is testing.

Deduplicating backtracking: sort, loop per level, skip equal siblings, prune by validity

💡 Fun fact: the duplicate-skip condition is i > start and nums[i] == nums[i-1] — and the i > start half is what makes it correct. Equal values must be skipped when they are siblings in the recursion tree, because a sibling branch has already generated everything that branch would. They must not be skipped when they are ancestors, because [1, 1] is a perfectly legitimate arrangement of two equal values. One comparison separates the two cases, and getting it wrong drops valid answers rather than duplicates.

🔓 The 6 problems in this chapter are free. Sign in with Google or Microsoft to start solving.


The one-line idea: sort first, then at every level skip a candidate equal to the one just tried at that same level. For validity-constrained enumeration, carry counters instead of generating-then-filtering, so the search only ever walks toward legal answers.


1. Subsets and combinations

def subsets(nums):
    nums.sort()
    result, path = [], []

    def backtrack(start):
        result.append(path[:])              # every node is an answer, not just leaves
        for i in range(start, len(nums)):
            if i > start and nums[i] == nums[i-1]:
                continue                    # sibling duplicate — skip
            path.append(nums[i])
            backtrack(i + 1)
            path.pop()                      # undo

    backtrack(0)
    return result

Two things to notice. result.append(path[:]) happens at every node, because every prefix is itself a valid subset — unlike permutations, where only complete leaves count. And path[:] copies; appending path itself stores a reference that later mutations will corrupt, which is one of the most common silent bugs in this family.

2. Permutations with duplicates

Permutations differ because every element is used exactly once, so you need a used array rather than a start index:

for i in range(len(nums)):
    if used[i]:
        continue
    if i > 0 and nums[i] == nums[i-1] and not used[i-1]:
        continue                            # the equal predecessor is unused → sibling duplicate
    used[i] = True
    path.append(nums[i])
    backtrack()
    path.pop(); used[i] = False

not used[i-1] is the permutation form of the same idea: if the identical previous value has not been used at this point, choosing this one now duplicates a branch that already ran.

3. Prune, do not filter

Generate Parentheses shows the alternative to duplicate-skipping — constrain the search so invalid states never appear at all:

def gen(path, open_used, close_used):
    if len(path) == 2 * n:
        result.append(''.join(path)); return
    if open_used < n:                       # can always open while any remain
        gen(path + ['('], open_used + 1, close_used)
    if close_used < open_used:              # only close what has been opened
        gen(path + [')'], open_used, close_used + 1)

Every leaf reached is a valid string. Nothing is generated and discarded, which is why this is O(Catalan(n)) rather than O(2^(2n)).


4. A 30-second worked example (why i > start matters)

nums = [1, 1, 2], generating subsets:

Why the i greater-than start guard matters, traced over nums 1,1,2

The same value is skipped in one place and kept in the other, and i > start is the only thing distinguishing them. Skipping both would lose [1,1]; skipping neither would emit [1] twice.


5. Where you'll actually meet this

  • Test-case generation. Enumerating distinct input combinations, where duplicates waste time without adding coverage.
  • Configuration search. Trying every valid combination of feature flags or parameters.
  • Security tooling. Enumerating candidate arrangements of a key space under constraints.
  • Predictive text. Letter Combinations is the original T9 keypad expansion.
  • Scheduling and seating. Distinct assignments where interchangeable people or slots must not multiply the result set.

6. Problems in this chapter

▶ Generate Parentheses

All valid combinations of n bracket pairs. Two counters constrain the search so every leaf is valid.
Pattern: counter-pruned generation. Target: O(Catalan(n)) results, O(n) recursion depth.

▶ Subsets

Every subset of a collection. Record at every node, not only at leaves.
Pattern: start-index backtracking. Target: O(2ⁿ · n) time.

▶ Permutations II

All distinct permutations of a collection containing duplicates. A used array plus the not used[i-1] skip.
Pattern: permutation backtracking with sibling-duplicate skipping. Target: O(n! · n) worst case, fewer with duplicates.

▶ Palindrome Permutation II

All palindromes formable by rearranging a string. Check feasibility by character parity first, then permute half the string and mirror it.
Pattern: feasibility check + half-permutation. Target: O((n/2)! · n).

▶ Letter Combinations of a Phone Number

Every string from a digit sequence on a phone keypad. Straight product enumeration, one digit per recursion level.
Pattern: cartesian-product backtracking. Target: O(4ⁿ · n).


7. Common pitfalls 🚫

  • Appending path instead of path[:]. Storing a reference means later mutations silently corrupt every recorded answer.
  • Deduplicating with a set at the end. It works, hides the real insight, and costs both time and memory.
  • Skipping duplicates without the i > start guard. Legitimate arrangements of equal values disappear.
  • Forgetting to sort. The duplicate skip relies on equal values being adjacent.
  • Recording only at leaves for subsets. Every node is an answer there; only permutations require completeness.
  • Permuting the whole string in Palindrome Permutation II. Permute half and mirror it.
  • Skipping the parity check. A string with two odd-count characters has no palindrome at all, and testing that first avoids an entire pointless search.
  • Binary Watch — every time displayable with exactly n LEDs lit. Enumeration over a tiny fixed space, and the neat version skips backtracking entirely by counting set bits over all 12 x 60 times.

8. Key takeaways

  1. Sort, then skip sibling duplicates. One line replaces post-filtering entirely.
  2. i > start (or not used[i-1]) is the whole subtlety — it separates siblings from ancestors.
  3. Copy the path when recording. References are the classic silent corruption.
  4. Subsets record at every node; permutations only at leaves.
  5. Prune with counters rather than filtering when validity can be maintained incrementally.
  6. Check feasibility before searching. A parity test can rule out the whole search space instantly.
  7. Why interviewers like it: everyone can enumerate. Producing distinct results without a deduplicating set, and explaining why the skip is correct, is the differentiator.

Order: Generate Parentheses → Subsets → Permutations II → Palindrome Permutation II → Letter Combinations of a Phone Number → Binary Watch.

Letter Combinations of a Phone Number

Core idea: Each digit 2–9 maps to a small set of letters (the old phone keypad). A combination picks exactly one letter per digit, in order. Backtrack position by position: at digit i, try each of its letters in turn — append it to the current path, recurse to digit i+1, then pop it off and try the next. When the path's length equals the number of digits, every position has been filled, so record the finished string. Empty input has no digits to choose, so the answer is []. There are at most 4 choices per digit, giving O(4ⁿ) combinations.

Problem, rephrased

You're building the T9 predictive-text layer for a feature phone: the user taps number keys, and before the dictionary filter can run, the software must expand the tap sequence into every word it could spell. You're given a string digits containing digits from 2–9, where each digit corresponds to a group of letters exactly as printed on a telephone keypad (2 -> abc, 3 -> def, …, 7 -> pqrs, 8 -> tuv, 9 -> wxyz).

Return all the letter strings the number could spell, choosing one letter for each digit, in order. The downstream filter needs every candidate, so none may be skipped and none duplicated; the output may be in any order. If digits is empty the user has typed nothing, so return [] — not [""].

Strip the story away and this is Letter Combinations of a Phone Number (LeetCode 17): given a string containing digits from 2–9, return all possible letter combinations the number could represent under the standard keypad mapping, in any order.

A worked input/output

digits Output Why
"23" ["ad","ae","af","bd","be","bf","cd","ce","cf"] 2 -> abc (3 choices) × 3 -> def (3 choices) = 9 strings.
"2" ["a","b","c"] One digit, one letter per combination.
"9" ["w","x","y","z"] 9 -> wxyz has 4 letters.
"" [] No digits → no combinations to form.

Notice the output size is the product of each digit's letter count: "23" gives 3 × 3 = 9; a number of all 7s and 9s multiplies 4s.

Sign in to continue reading

The rest of this lesson is available with a free account. Signing in with Google or Microsoft is free.

Sign in to read the full lesson

Sign in to MAANG.io

Use your Google or Microsoft account — no password to remember.

Continue with Google Continue with Microsoft

Please accept the terms above to continue.