1

Professor Phillip Fuchs has a very neat algorithm on generating permutation of an array of objects using an pure iterative method (see quickperm.org). There are basically 2 variations, a countdown algorithm, and a counting (count-up) algorithm. The ideas are the same, just a difference in the matter of initialization and whether to increment or decrement the "odometer" digits.

I have looked into the algorithm for a while, and can understand the idea, except for one particular detail. In the algorithm, as well as the explanation that Prof. Fuchs' provided for his algorithm, it says that if the high index is even, the low index is set to 0; and if the high index is odd, the low index is set to the "odometer" digit pointed to by the high index (the original text uses j, i, and p[]):

lowIndex = (highIndex %2 == 0) ? 0 : odometerDigit[highIndex];

I am just having a hard time trying to understand this logic. Why does the highIndex being even or odd determines the lowIndex should be 0 or the value in the odometerDigit? Prof. Fuchs' doesn't provide a more detailed explanation for this, yet this is the "magic" crux of the algorithm. I would really appreciate any insights that can help me further understand this.

Jim
  • 161
  • 2
  • 10
  • Have you tried stepping it by hand? – David Eisenstat Apr 15 '13 at 21:45
  • As a matter of fact I had. I stepped through by hand with a 3-element array, a 4-element array, and a 5-element array (took quite a while). There definitely was some pattern, but I just wasn't able to pinpoint exactly what. I also tried to search for algorithms that permutes by swapping elements, hoping it would help me further understand the swapping pattern in this algorithm, but didn't find any. Here is a related post http://stackoverflow.com/questions/11915026/permutations-of-a-string-using-iteration but the answerer also just gave the code and dodged any explanations – Jim Apr 16 '13 at 01:16

1 Answers1

2

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)
David Eisenstat
  • 64,237
  • 7
  • 60
  • 120
  • David, thank you so much for such detailed explanation and it must have taken you hours. I still need to take my time to digest the information you presented here. – Jim Apr 16 '13 at 18:04
  • @Jim I myself was surprised at how subtle it turned out to be. If you don't mind drinking from a firehouse, you might be interested also in Knuth's treatment in TAoCP. – David Eisenstat Apr 16 '13 at 18:16
  • David, funny that you mentioned TAoCP. I was just thinking the other day, debating between buying TAoCP or ConCrete Mathematics. – Jim Apr 16 '13 at 18:49