Since you stepped N = 5 (!), I'll take a stab at this.
Fuchs's [citation needed] attitudes about recursion aside, I think one way to explain what's going on is that he's tinkered with the following recursive algorithm. I hope this one isn't magic to you.
def permute0(lst, n):
if n < 2:
print(lst)
else:
for j in range(n - 1, -1, -1): # j in [0, n) descending
# generate permutations with lst[n - 1] fixed
lst[j], lst[n - 1] = lst[n - 1], lst[j] # swap
permute0(lst, n - 1)
lst[j], lst[n - 1] = lst[n - 1], lst[j] # swap back
The first optimization is that, in the interior stack frames, only the value of the variable j needs be stored, in the array that he calls p. I'm not going to bother with that one.
The second optimization is that he's done a swap-ectomy. We can reduce the swapping a little by not swapping an element with itself.
def permute1(lst, n):
if n < 2:
print(lst)
else:
permute1(lst, n - 1)
for j in range(n - 2, -1, -1):
lst[j], lst[n - 1] = lst[n - 1], lst[j]
permute1(lst, n - 1)
lst[j], lst[n - 1] = lst[n - 1], lst[j]
To reduce the number of swaps further, we're going to have to be cleverer. permute2, unlike the previous versions, does not restore lst before returning. permute2 is not recursive because it uses permute1 to do the dirty work.
def permute2(lst, n):
if n < 2:
print(lst)
else:
permute1(lst, n - 1)
for j in range(n - 2, -1, -1):
lst[j], lst[n - 1] = lst[n - 1], lst[j]
permute1(lst, n - 1)
What does permute2 do to lst? Ignoring the calls to permute1, which leave lst unchanged, it rotates lst one element to the left. Now we could write permute2a, which calls permute2 and looks for the next element to swap into lst[n - 1] where permute2 put it, but we can't write a whole tower of permutes.
We need an inductive hypothesis about the behavior of our not-yet-extant permute function that we can use to prove the next one up. This is a creative act, and the source of a lot of "magic" in computer science. What I'm about to write is probably not a realistic depiction of how I think.
The class of algorithms we're looking in has two natural constraints. First, these algorithms make the minimum number of swaps. Second, they're self-similar and exhaust all permutations on the first N - 1 elements before moving the last. Together, these constraints force one of the elements swapped to be stored in the location corresponding to Fuchs's i.
N = 0: there's only one permutation
N = 1: there's only one permutation
N = 2: the graph looks like 0 1 - 1 0
N = 3: the graph looks like
0 1 2 1 2 0 2 0 1
| | |
-------+------- all bipartite connections
| | |
0 2 1 2 1 0 1 0 2
Start without loss of generality at 0 1 2. (The initial contents of lst do not matter.) Then we have to start
0 1 2 # forced w.l.o.g.
1 0 2 # forced because 2 cannot be moved yet.
The only interesting choice arises here. We could finish
2 0 1 # chose 1
0 2 1 # forced because 1 cannot be moved yet
1 2 0 # forced because we must move element 1 and 0 1 2 is already visited
2 1 0 # forced because 0 cannot be moved yet.
The other possible continuation is
1 2 0 # chose 0
2 1 0 # forced because 0 cannot be moved yet
2 0 1 # forced because we must move element 0 and 0 1 2 is already visited
0 2 1 # forced because 1 cannot be moved yet.
At this point, I notice that N = 2 and the first possibility for N = 3 both reverse the permutation. I'm going to try to construct permute3 that reverses lst. Let's see what it might do for N = 4.
0 1 2 3
...
2 1 0 3
3 1 0 2
...
0 1 3 2
0 2 3 1
...
3 2 0 1
3 2 1 0
...
1 2 3 0 # oops
Well, that didn't work. We need an odd number of recursive calls to leave the subarray reversed. That's the first suggestion that maybe we need different behavior for i odd and i even.
Like permute2, for N = 4, this hypothetical algorithm rotates the elements one to the left. Variations on that theme seem to be dead ends. It's late enough here, and I've done enough fruitless experimentation, that Fuchs's algorithm is starting to seem magical to me as well.
Let me write permute2a after all. Recall that permute2 rotates lst one element to the left. If permute2a makes swaps with position j increasing while compensating for the rotations, it swaps the same position repeatedly (say 0, since no other position is guaranteed to be accessible).
def permute2a(lst, n):
if n < 2:
print(lst)
else:
permute2(lst, n - 1)
for j in range(n - 2, -1, -1):
lst[0], lst[n - 1] = lst[n - 1], lst[0]
permute2(lst, n - 1)
Now, an aha moment where I realize that, if permute2 called permute2a instead of permute1, the pair almost would be equivalent to Fuchs's algorithm. This still seems magical to me, but it's time for me to call it a day. Maybe tomorrow.
In fact, permute2a can handle not just a left rotation but any fixed permutation from permute2 that is a cycle, i.e., that a permutation that, when applied repeatedly to a single element in its domain, gives every other element. Given that permute2 behaves this way, the effect of permute2a is to apply permute2's (N - 1)-cycle permutation N times, except it swaps elements in and out of position 0 in between cycles, with the effect that each element is moved along the cycle N - 1 times, which has no effect. The effect of permute2a on lst, regardless of permute2, is thus to swap positions 0 and N - 1.
Now all we have to do is argue that permute2 can use permute2a. We're helped a lot by the fact that the behavior of permute2a is insensitive to the implementation details of permute2. Moreover, since permute2a touches only the first and last element of the subarray, the current implementation of permute2 is most of the way there. In fact, if N is even, it works as is.
0123
...
2103
2130
...
3120
3021
...
2031
1032
...
3012
The problem with N odd is that the same element will be swapped twice into last position.
01234
...
31204
31240
...
41230
41032
...
31042
32041
...
42031
12034 # oops
All we have to do now is show that the new permute2 cycles its elements (when N is even). I'm going to use a little group theory because I can't see a simple elementary proof. In cycle notation, the permutation is
(0 n-2)(0 n-1)(0 n-2)(1 n-1)(0 n-2)...(0 n-2)(n-3 n-1)(0 n-2)(n-2 n-1)(0 n-2).
Disjoint cycles commute.
(0 n-2)(0 n-1)[(0 n-2)...n-2 times...(0 n-2)](1 n-1)...(n-3 n-1)(n-2 n-1)(0 n-2).
Since n is even, n-2 consecutive swaps has no effect.
(0 n-2)(0 n-1)(1 n-1)...(n-3 n-1)(n-2 n-1)(0 n-2).
The sequence (0 n-1)(1 n-1)...(n-3 n-1)(n-2 n-1), as we observed before, is a cycle.
(0 n-2)(0 n-1 n-2 ... 1)(0 n-2).
This is a conjugated cycle, which is also a cycle.
(0 n-3 n-4 ... 1 n-2 n-1).
Here's the final version. I claim that it's equivalent to Fuchs's algorithm modulo the explicit stack.
def permute3(lst, n):
if n < 2:
print(lst)
else:
permute3(lst, n - 1)
for k in range(n - 2, -1, -1):
i = n - 1
j = 0 if i % 2 == 0 else k
lst[j], lst[i] = lst[i], lst[j]
permute3(lst, n - 1)