From Backtracking to Factorial Coordinates: Understanding Permutations Deeply

/software-computational-architecture

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