Most developers learn permutation generation through recursion and swapping.
It works, it's elegant… but it often feels a bit magical.
This article goes much deeper.
We will:
- Fully understand how backtracking actually works
- Build a precise mental model of recursion
- Discover that permutations form a structured coordinate space
- Eliminate recursion entirely using factorial number systems (Lehmer code)
- Derive efficient, controllable, high-performance implementations
1. The Classical Backtracking Algorithm
Here is the standard implementation:
template <typename T>
void permuteHelper(std::vector<T>& vec, int start, std::vector<std::vector<T>>& result) {
if (start >= vec.size()) {
result.push_back(vec);
return;
}
for (int i = start; i < vec.size(); ++i) {
std::swap(vec[start], vec[i]);
permuteHelper(vec, start + 1, result);
std::swap(vec[start], vec[i]); // backtrack
}
}
2. The First Misconception
A very common intuition is:
"A swap creates a permutation"
This is wrong.
Reality:
- A swap = a choice
- Recursion = completing that choice
- Base case = validating the permutation
3. The Real Meaning of start
start is not just an index.
It represents:
How many positions are already fixed
| start | meaning |
|---|---|
| 0 | nothing fixed |
| 1 | first element fixed |
| 2 | first 2 fixed |
| n | fully fixed → valid permutation |
4. Why We Push at the Base Case
if (start >= vec.size()) {
result.push_back(vec);
}
This is the only correct place to store a permutation. Because only here:
All positions are fixed
Everything before that is just partial construction.
5. The Hidden Tree Structure
The algorithm is actually exploring a tree:
position 0 → choose element
position 1 → choose element
position 2 → choose element
...
→ complete → store
6. The First Permutation Mystery
Why is [1,2,3,4,5] the first result?
Because the algorithm always tries:
i = start
Which means:
swap(start, start) → no change
So the first path is:
[1,2,3,4,5] → unchanged all the way down → pushed (because always i == start)
7. The Key Insight: Subtrees Have Identity Paths
Example:
swap(0,1) → [2,1,3,4,5]
Then:
start = 1 → no swaps
start = 2 → no swaps
...
So we get:
[2,1,3,4,5]
This reveals something deep:
Every subtree has a path where nothing changes further.
8. From Tree to Coordinates
Each recursive path corresponds to a sequence:
i₀, i₁, i₂, ..., iₙ₋₁
Where:
iₖ ∈ [k, n-1]
Normalize:
cₖ = iₖ - k
Now:
c₀ ∈ [0, n-1]
c₁ ∈ [0, n-2]
...
cₙ₋₁ = 0
9. This Is a Number System
These cₖ form a number in:
factorial base (mixed radix)
Example for n = 3:
| permutation | c |
|---|---|
| [1,2,3] | [0,0,0] |
| [1,3,2] | [0,1,0] |
| [2,1,3] | [1,0,0] |
| [2,3,1] | [1,1,0] |
| [3,2,1] | [2,0,0] |
| [3,1,2] | [2,1,0] |
10. Backtracking = Counting
The recursion is actually doing:
for c₀ in [0..n-1]
for c₁ in [0..n-2]
...
emit permutation
This is just counting:
0 → 1 → 2 → ... → n! - 1
11. Killing the Tree
Instead of exploring all possibilities:
We can directly follow one path.
At level k:
i = k + c[k]
12. Direct Construction (No Branching)
template <typename T>
std::vector<T> buildPermutation(std::vector<T> vec, const std::vector<int>& c) {
int n = vec.size();
for (int k = 0; k < n; ++k) {
int i = k + c[k];
std::swap(vec[k], vec[i]);
}
return vec;
}
13. Converting Index → Permutation
We convert integer k into factorial base:
std::vector<int> toFactorialBase(int k, int n) {
std::vector<int> c(n);
for (int i = 1; i <= n; ++i) {
c[n - i] = k % i;
k /= i;
}
return c;
}
14. Full k-th Permutation
template <typename T>
std::vector<T> kthPermutation(std::vector<T> elements, int k) {
auto c = toFactorialBase(k, elements.size());
return buildPermutation(elements, c);
}
15. Performance Comparison
| Method | Complexity |
|---|---|
| Backtracking | O(n!) |
| Direct decoding | O(n²) |
| With tree structure | O(n log n) |
16. Mental Model Summary
Backtracking:
explore tree
Coordinates:
follow path
Factorial system:
index permutations
17. Deep Insight
A permutation is not just an arrangement.
It is a coordinate in a structured space.
18. Practical Uses
- Parallel permutation search
- Optimization problems (TSP, scheduling)
- Procedural generation
- Deterministic randomness
- Memory-efficient enumeration
19. Final Takeaway
Backtracking explores permutations.
Factorial encoding defines them.
Once you see permutations as coordinates:
- recursion becomes optional
- control becomes explicit
- performance becomes predictable