bigonotes
DSA: Patterns & PythonBacktracking

Permutations

Build every ordering by trying each unused element at each position, skip duplicate siblings, and prune at placement time.

4 min readLast updated

Permutations are selections where order matters: [1, 2] and [2, 1] are different answers. Subsets and combinations only look forward from a start index. Here every position may take any element not used yet. The tree has N choices at the first level, N − 1 at the next, and so on, which gives N! leaves.

The idea#

Build the answer one position at a time:

  1. Choices: every element not already in the path, so the loop always runs over all n indices with a used check.
  2. State: the path so far and a used array (or set).
  3. Goal: the path has n elements; snapshot a copy.

Why it works: each level fixes one position, and the used check guarantees every element appears exactly once. Every ordering is one root-to-leaf path, so none is missed and none repeats (when the values are distinct).

The cost follows from the size of that tree. There are N!N! leaves and each copy costs O(N)O(N), so time is O(N⋅N!)O(N \cdot N!). The recursion is N deep and the used array is size N, so the extra space is O(N)O(N), not counting the output.

Slicing vs a used array#

You need some way to track which elements are still free, and the choice affects memory. A tempting version passes the remaining elements down by slicing: find(path + [x], rest[:i] + rest[i + 1:]). It's correct, but each level makes new lists, and all N frames are alive at once: N + (N − 1) + … + 1 = O(N2)O(N^2) extra memory. The used array with append and pop keeps it at O(N)O(N).

Duplicate values#

When the input has repeated values, two equal elements produce the same orderings twice. The fix is to sort first, so equal values sit next to each other, and then force equal values into a fixed order. Working out the exact skip condition is part of Permutations II.

How to spot it#

These cues point to permutations.

  • "All permutations", "all arrangements", "all orderings".
  • Each position must take a different element.
  • Constraints such as N ≤ 8 to 12, where N! is still affordable.
  • A permutation with rules about where each element may go (divisibility, adjacency, attacking queens).

Template#

The template fills one position per call by trying every unused element there. It also has a slot for placement rules, explained right after it.

track():
    if length of path == n:
        record a copy of path
        return
    for i from 0 to n - 1:             # every position sees every element
        if used[i]: continue
        if placing items[i] here breaks a rule: continue   # prune early
        mark i used; append items[i] to path
        track()
        pop from path; unmark i
track()

Check placement rules in the loop, before you place the value, not at the leaf. A bad prefix is then cut immediately instead of being completed in every way and then rejected.

Pitfalls#

Watch for these when moving from combinations to permutations.

  • Looping from start. That generates combinations, not permutations; permutations always loop from 0.
  • Forgetting to reset used[i] after the recursive call.
  • Deduplicating with path not in res. It works, but each check scans every result so far. Sort and skip siblings instead.
  • Under-counting memory with slicing. It's O(N2)O(N^2) auxiliary, not O(N)O(N).

Problems in this pattern#

These problems use this pattern. Solve them before you open the hints below. Start with Permutations.

Hints#

Open a hint only after you have tried the problem for a while.

Hint: Permutations

The values are distinct, so the template works with no placement rule. Remember to record a copy (path[:]), not the live list.

Hint: Permutations II

Sort first. Skip nums[i] when i > 0, nums[i] == nums[i - 1] and used[i - 1] is False. That forces equal values to be used left to right: the second 1 may only follow the first, never stand in for it, so each distinct ordering appears once.

Hint: Permutations II: slicing style

If you pass sliced remaining lists instead, sort first and skip rest[i] == rest[i - 1] for i > 0. This works because removing one element from a sorted list leaves it sorted, so equal values stay neighbors at every level.

Hint: Beautiful Arrangement

Position p (1-indexed) may take value v only if p % v == 0 or v % p == 0. Check this before you place the value, so bad prefixes die early.

Hint: N-Queens

One queen per row means a solution is a permutation of the columns with extra diagonal rules. The constraint sets that make each check O(1)O(1) are covered in Grid & Constraint Boards.

Key takeaways#

  • Order matters, so every position loops over all elements, filtered by used.
  • Time is O(N⋅N!)O(N \cdot N!); extra space is O(N)O(N) with a used array, O(N2)O(N^2) with slicing.
  • Duplicates: sort, then force equal values into a fixed order.
  • Check placement rules before recursing, not at the leaf.
Revision notes · 1 min

Core idea#

In a permutation, order matters. Every position may take any element not used yet, so the loop always starts at 0 and checks a used array.

Key points#

  • State: the path and a used array. Goal: the path has n elements.
  • The tree has N! leaves. Time is O(N⋅N!)O(N \cdot N!) with copying.
  • Extra space is O(N)O(N) with a used array and append/pop.
  • Passing sliced remaining lists costs O(N2)O(N^2) extra memory, because all N frames hold new lists.
  • Duplicates: sort, then skip nums[i] == nums[i - 1] when used[i - 1] is false. Equal values then go in left to right.
  • Check placement rules before you place a value, not at the leaf. Beautiful Arrangement prunes on p % v == 0 or v % p == 0.
  • Spot it: "all arrangements or orderings", N ≤ 8 to 12, rules about where each element may go.

Template#

sort items                           # equal values become neighbors
track():
    if length of path == n: record a copy of path; return
    for i from 0 to n - 1:
        if used[i]: continue
        if i > 0 and items[i] == items[i - 1] and not used[i - 1]:
            continue                 # same value tried as a sibling
        mark i used; append items[i] to path
        track()
        pop from path; unmark i

Pitfalls#

  • Looping from start. That generates combinations.
  • Forgetting to reset used[i] after the call.
  • Deduplicating with path not in res. Each check scans every result.
  • Calling the slicing version O(N)O(N) space. It is O(N2)O(N^2).

Say it in an interview#

  • "Order matters, so each position loops over all elements, filtered by used."
  • "N factorial leaves, each copied in O(N), so O(N times N factorial)."

Found a mistake? Email ask@bigonotes.com