Subsets and permutations¶
Generate every subset and every ordering of a list by deciding one item at a time and undoing each decision afterwards. The same pattern, with a check that cuts off hopeless branches, finds the subsets with a given sum.
| Subsets | Permutations | Subset sum (with pruning) | Extra space |
|---|---|---|---|
| \(O(n \cdot 2^n)\) | \(O(n \cdot n!)\) | at most \(O(2^n)\), often far less | \(O(n)\) besides the output |
The idea¶
Subsets. Each item is either out or in: two choices per item, \(2^n\) subsets. Picture a tree: the root has decided nothing; each level decides one item; each of the \(2^n\) leaves is one subset. A recursive function walks this tree: decide item \(i\) (first "out", then "in"), recurse to item \(i + 1\), and when all items are decided, record the subset.
Permutations. Fill position 1 with any of the \(n\) items, position 2 with any of the \(n - 1\) left, and so on:
\(n!\) orderings. A used list says which items are still free.
Subset sum. Look for the subsets that add up to a target. With positive numbers, once the running total is above the target, nothing deeper can work, so return at once: pruning.
How it works¶
- Subsets:
go(i): ifi == n, recordchosen. Otherwisego(i + 1); appenditems[i];go(i + 1); pop. - Permutations:
go(): ifchosenis full, record it. Otherwise for each unused item: mark it, append,go(), pop, unmark. - Subset sum: like subsets, carrying the running total, and returning when it equals the target (record) or passes it (prune).
Code¶
def subsets(items):
"""Every subset: for each item, first leave it out, then put it in."""
out, chosen = [], []
def go(i):
if i == len(items):
out.append(list(chosen))
return
go(i + 1) # without items[i]
chosen.append(items[i]) # choose
go(i + 1) # with items[i]
chosen.pop() # un-choose (the "back" in backtracking)
go(0)
return out
def permutations(items):
"""Every ordering: fill the positions one by one with an item not used yet."""
out, chosen, used = [], [], [False] * len(items)
def go():
if len(chosen) == len(items):
out.append(list(chosen))
return
for i, x in enumerate(items):
if not used[i]:
used[i] = True; chosen.append(x)
go()
chosen.pop(); used[i] = False
go()
return out
def subset_sums(items, target):
"""Every subset (keeping the items' order) that adds up to target; all items positive.
Prunes: stop as soon as the running sum is too big."""
out, chosen = [], []
def go(i, total):
if total == target:
out.append(list(chosen))
return
if i == len(items) or total > target: # nothing left, or already too much: give up on this branch
return
chosen.append(items[i])
go(i + 1, total + items[i])
chosen.pop()
go(i + 1, total)
go(0, 0)
return out
if __name__ == "__main__":
print(subsets([1, 2, 3]))
print(permutations([1, 2, 3]))
print(subset_sums([3, 34, 4, 12, 5, 2], 9))
import java.util.ArrayList;
import java.util.List;
public class SubsetsPermutations {
static List<List<Integer>> out;
static List<Integer> chosen;
/** Every subset: for each item, first leave it out, then put it in. */
static List<List<Integer>> subsets(int[] items) {
out = new ArrayList<>(); chosen = new ArrayList<>();
subsetsGo(items, 0);
return out;
}
static void subsetsGo(int[] items, int i) {
if (i == items.length) { out.add(new ArrayList<>(chosen)); return; }
subsetsGo(items, i + 1); // without items[i]
chosen.add(items[i]); // choose
subsetsGo(items, i + 1); // with items[i]
chosen.remove(chosen.size() - 1); // un-choose (the "back" in backtracking)
}
/** Every ordering: fill the positions one by one with an item not used yet. */
static List<List<Integer>> permutations(int[] items) {
out = new ArrayList<>(); chosen = new ArrayList<>();
permGo(items, new boolean[items.length]);
return out;
}
static void permGo(int[] items, boolean[] used) {
if (chosen.size() == items.length) { out.add(new ArrayList<>(chosen)); return; }
for (int i = 0; i < items.length; i++)
if (!used[i]) {
used[i] = true; chosen.add(items[i]);
permGo(items, used);
chosen.remove(chosen.size() - 1); used[i] = false;
}
}
/** Every subset (keeping the items' order) that adds up to target; all items positive.
* Prunes: stop as soon as the running sum is too big. */
static List<List<Integer>> subsetSums(int[] items, int target) {
out = new ArrayList<>(); chosen = new ArrayList<>();
sumGo(items, target, 0, 0);
return out;
}
static void sumGo(int[] items, int target, int i, int total) {
if (total == target) { out.add(new ArrayList<>(chosen)); return; }
if (i == items.length || total > target) return; // nothing left, or already too much
chosen.add(items[i]);
sumGo(items, target, i + 1, total + items[i]);
chosen.remove(chosen.size() - 1);
sumGo(items, target, i + 1, total);
}
public static void main(String[] args) {
System.out.println(subsets(new int[]{1, 2, 3}));
System.out.println(permutations(new int[]{1, 2, 3}));
System.out.println(subsetSums(new int[]{3, 34, 4, 12, 5, 2}, 9));
}
}
#include <iostream>
#include <string>
#include <vector>
using namespace std;
using List = vector<vector<int>>;
// Every subset: for each item, first leave it out, then put it in.
void subsetsGo(const vector<int>& items, size_t i, vector<int>& chosen, List& out) {
if (i == items.size()) { out.push_back(chosen); return; }
subsetsGo(items, i + 1, chosen, out); // without items[i]
chosen.push_back(items[i]); // choose
subsetsGo(items, i + 1, chosen, out); // with items[i]
chosen.pop_back(); // un-choose (the "back" in backtracking)
}
// Every ordering: fill the positions one by one with an item not used yet.
void permGo(const vector<int>& items, vector<bool>& used, vector<int>& chosen, List& out) {
if (chosen.size() == items.size()) { out.push_back(chosen); return; }
for (size_t i = 0; i < items.size(); i++)
if (!used[i]) {
used[i] = true; chosen.push_back(items[i]);
permGo(items, used, chosen, out);
chosen.pop_back(); used[i] = false;
}
}
// Every subset (keeping the items' order) that adds up to target; all items positive.
// Prunes: stop as soon as the running sum is too big.
void sumGo(const vector<int>& items, int target, size_t i, int total, vector<int>& chosen, List& out) {
if (total == target) { out.push_back(chosen); return; }
if (i == items.size() || total > target) return; // nothing left, or already too much
chosen.push_back(items[i]);
sumGo(items, target, i + 1, total + items[i], chosen, out);
chosen.pop_back();
sumGo(items, target, i + 1, total, chosen, out);
}
string show(const List& L) {
string s = "[";
for (size_t k = 0; k < L.size(); k++) {
s += k ? ", [" : "[";
for (size_t j = 0; j < L[k].size(); j++) s += (j ? ", " : "") + to_string(L[k][j]);
s += "]";
}
return s + "]";
}
int main() {
vector<int> chosen;
List out;
subsetsGo({1, 2, 3}, 0, chosen, out);
cout << show(out) << "\n";
out.clear();
vector<bool> used(3, false);
permGo({1, 2, 3}, used, chosen, out);
cout << show(out) << "\n";
out.clear();
sumGo({3, 34, 4, 12, 5, 2}, 9, 0, 0, chosen, out);
cout << show(out) << "\n";
}
Output: the 8 subsets of [1, 2, 3], starting with the empty one; the 6 permutations in dictionary order; and
[[3, 4, 2], [4, 5]], the two subsets of [3, 34, 4, 12, 5, 2] that add up to 9.
When to use it¶
- Trying every combination when \(n\) is small (up to about 20–25 for subsets, 10–11 for permutations).
- Brute-force checks of a cleverer algorithm on small inputs: generate all cases and compare.
- Puzzles and planning: choosing a team, ordering stops on a route (the travelling salesman for a few cities), assigning tasks.
- With pruning, much larger inputs can still be searched when the rules cut most branches.
Common mistakes¶
- Recording the shared list itself instead of a copy: every recorded answer then changes as the search continues, and they all end up empty.
- Forgetting to undo (
pop,used[i] = False): later branches see a wrong state. - Pruning on a sum with negative numbers: the total can come back down, so "too big" does not mean hopeless.
- Duplicates in the input:
[1, 1, 2]would give repeated subsets and permutations; sort and skip an item equal to the previous one at the same level.
Going deeper¶
Each part below starts closed; open the ones you want.
Sample walkthrough: subsets of [1, 2, 3] and the sum 9
The order the subsets come out (0 = out, 1 = in, for items 1, 2, 3):
| Decisions | Subset |
|---|---|
| 0 0 0 | {} |
| 0 0 1 | {3} |
| 0 1 0 | {2} |
| 0 1 1 | {2, 3} |
| 1 0 0 | {1} |
| 1 0 1 | {1, 3} |
| 1 1 0 | {1, 2} |
| 1 1 1 | {1, 2, 3} |
The decisions count from 0 to 7 in binary: subsets of \(n\) items and \(n\)-bit numbers are the same thing.
Subset sum 9 from [3, 34, 4, 12, 5, 2]: take 3; take 34 → 37 > 9, prune; skip 34; take 4 → 7; take 12 → 19, prune; skip 12; take 5 → 12, prune; skip 5; take 2 → 9, found [3, 4, 2]. Later, skipping 3: ... take 4, skip 12, take 5 → 9, found [4, 5].
The math: counting, and the cost of listing
Counting. Each item has 2 choices independently, so there are \(2^n\) subsets; \(\sum_k \binom{n}{k} = 2^n\) groups them by size. Orderings: \(n\) choices for the first position, \(n - 1\) for the second, ..., so \(n! = n(n-1)\cdots 1\).
Tree size. The subset tree has \(2^{n+1} - 1\) nodes. The permutation tree has \(\sum_{k=0}^{n} \frac{n!}{(n-k)!} = n! \sum_{j=0}^{n} \frac{1}{j!} < e \cdot n!\) nodes.
Time. Each answer costs \(O(n)\) to copy, so listing all subsets is \(\Theta(n \, 2^n)\) and all permutations \(\Theta(n \cdot n!)\). No algorithm can list them faster than the size of its output.
How fast these grow. \(2^{30} \approx 10^9\); \(10! = 3{,}628{,}800\); \(13! \approx 6.2 \times 10^9\); \(20! \approx 2.4 \times 10^{18}\). Stirling's formula: \(n! \approx \sqrt{2\pi n}\,(n/e)^n\).
Subset sum is NP-complete, so no polynomial algorithm is known; with small numbers, the knapsack-style DP solves it in \(O(n \cdot \text{target})\).
Watch it step by step
The tree of decisions for {1, 2, 3}: a left branch leaves the item out, a right branch (+k) puts it in. Red is where the search is, green has been visited, purple are the subsets found.
Variations and related ideas
- Bitmasks: loop
maskfrom 0 to \(2^n - 1\); bit \(i\) says whether item \(i\) is in. Short and fast for \(n \le 20\) or so. - Next permutation: step from one ordering to the next in dictionary order without recursion
(
std::next_permutationin C++). - Combinations (subsets of exactly \(k\) items): stop going deeper once \(k\) are chosen, or when too few items remain.
- Gray code: list subsets so that consecutive ones differ by one item.
- Heap's algorithm: each permutation from the previous by a single swap.
- Meet in the middle: for subset sum with \(n \approx 40\), list the \(2^{20}\) sums of each half and match them.
See also¶
References¶
- D. E. Knuth, The Art of Computer Programming, Vol. 4A, Combinatorial Algorithms, Part 1, Section 7.2.1 (generating all possibilities).
- S. S. Skiena, The Algorithm Design Manual, 3rd ed., Springer, 2020, Chapter 9 (combinatorial search).