Generating Permutations and Subsets
Generating every permutation, subset, or combination of a set is a fixed pattern of recursive choices, and knowing the counting formula upfront tells you whether "generate them all" is even feasible.
Prerequisites: Backtracking Search, Big-O Complexity
"Try every possible ordering" or "check every possible subset" comes up constantly — in brute-forcing a small combinatorial optimization, in enumerating candidate portfolios of names out of , in generating test cases. The generation itself is a small, fixed set of recursive patterns; the part that actually matters is knowing how fast the count explodes, because that tells you the size of where "just enumerate them" stops being a plan.
The idea: one recursive choice per position, three variants
Permutations of distinct items: at each of positions, choose one of the remaining unused items, recurse, then un-choose it (classic backtracking). There are permutations — million, is already 1.3 trillion.
Subsets (the power set) of items: at each item, decide include-or-exclude, recurse either way. There are subsets — , is already 1 trillion. This is exactly the shape used by bitmask DP, where each subset is represented as an integer.
Combinations of size out of : like subsets, but you stop once items are chosen and never reconsider earlier positions once passed, avoiding the same combination being generated in more than one order. The count is , which is far smaller than when is far from — versus .
Worked example: subsets of [1, 2, 3]
Include-or-exclude on 1, then 2, then 3, backtracking-style: exclude 1 → exclude 2 → exclude 3 gives {}; exclude 1 → exclude 2 → include 3 gives {3}; exclude 1 → include 2 → exclude 3 gives {2}; exclude 1 → include 2 → include 3 gives {2,3}; and mirroring all four with 1 included gives {1}, {1,3}, {1,2}, {1,2,3}. Eight subsets total, matching .
def subsets(nums): # O(2^n) time and output size
result, partial = [], []
def backtrack(i):
if i == len(nums):
result.append(partial.copy())
return
backtrack(i + 1) # exclude nums[i]
partial.append(nums[i])
backtrack(i + 1) # include nums[i]
partial.pop()
backtrack(0)
return result
Permutations grow as , subsets as , combinations of size as — memorize these three growth rates, because they tell you instantly whether brute-force enumeration is viable for a given before you write a line of code.
If a problem says "choose of " rather than "all subsets," generate combinations directly (loop from the last chosen index forward) rather than generating all subsets and filtering by size — the difference between and work is often enormous.
Where it shows up
Interview staples: permutations, subsets, subsets II (with duplicates — sort first, skip repeated siblings), combination sum, letter case permutation. Interviewers watch for whether you know the count before writing the recursion, since it signals you understand the complexity you're about to generate. On a desk, brute-forcing a small basket of candidate assets for a discrete allocation problem, or enumerating feasible sub-portfolios under a cardinality constraint, both rely on the exact same generation patterns and the same awareness of when has grown too large to enumerate.
Practice in interviews
Further reading
- Cormen, Leiserson, Rivest & Stein, Introduction to Algorithms (ch. C, combinatorics appendix)