How to iterate through array combinations with constant sum efficiently? - algorithm

I have an array and its length is X. Each element of the array has range 1 .. L. I want to iterate efficiently through all array combinations that has sum L.
Correct solutions for: L = 4 and X = 2
1 3
3 1
2 2
Correct solutions for: L = 5 and X = 3
1 1 3
1 3 1
3 1 1
1 2 2
2 1 2
2 2 1
The naive implementation is (no wonder) too slow for my problem (X is up to 8 in my case and L is up to 128).
Could anybody tell me how is this problem called or where to find a fast algorithm for the problem?
Thanks!

If I understand correctly, you're given two numbers 1 ≤ X ≤ L and you want to generate all sequences of positive integers of length X that sum to L.
(Note: this is similar to the integer partition problem, but not the same, because you consider 1,2,2 to be a different sequence from 2,1,2, whereas in the integer partition problem we ignore the order, so that these are considered to be the same partition.)
The sequences that you are looking for correspond to the combinations of X − 1 items out of L − 1. For, if we put the numbers 1 to L − 1 in order, and pick X − 1 of them, then the lengths of intervals between the chosen numbers are positive integers that sum to L.
For example, suppose that L is 16 and X is 5. Then choose 4 numbers from 1 to 15 inclusive:
Add 0 at the beginning and 16 at the end, and the intervals are:
and 3 + 4 + 1 + 6 + 2 = 16 as required.
So generate the combinations of X − 1 items out of L − 1, and for each one, convert it to a partition by finding the intervals. For example, in Python you could write:
from itertools import combinations
def partitions(n, t):
"""
Generate the sequences of `n` positive integers that sum to `t`.
"""
assert(1 <= n <= t)
def intervals(c):
last = 0
for i in c:
yield i - last
last = i
yield t - last
for c in combinations(range(1, t), n - 1):
yield tuple(intervals(c))
>>> list(partitions(2, 4))
[(1, 3), (2, 2), (3, 1)]
>>> list(partitions(3, 5))
[(1, 1, 3), (1, 2, 2), (1, 3, 1), (2, 1, 2), (2, 2, 1), (3, 1, 1)]
There are (L − 1)! / (X − 1)!(L − X)! combinations of X − 1 items out of L − 1, so the runtime of this algorithm (and the size of its output) is exponential in L. However, if you don't count the output, it only needs O(L) space.
With L = 128 and X = 8, there are 89,356,415,775 partitions, so it'll take a while to output them all!
(Maybe if you explain why you are computing these partitions, we might be able to suggest some way of meeting your requirements without having to actually produce them all.)

Related

Advanced Algorithms Problems ("Nice Triangle"): Prime number Pyramid where every number depends on numbers above it

I'm currently studying for an advanced algorithms and datastructures exam, and I simply can't seem to solve one of the practice-problems which is the following:
1.14) "Nice Triangle"
A "nice" triangle is defined in the following way:
There are three different numbers which the triangle consists of, namely the first three prime numbers (2, 3 and 5).
Every number depends on the two numbers below it in the following way.
Numbers are the same, resulting number is also the same. (2, 2 => 2)
Numbers are different, resulting number is the remaining number. (2, 3 => 5)
Given an integer N with length L, corresponding to the base of the triangle, determine the last element at the top
For example:
Given N = 25555 (and thus L = 5), the triangle looks like this:
2
3 5
2 5 5
3 5 5 5
2 5 5 5 5
=> 2 is the result of this example
What does the fact that every number is prime have to do with the problem?
By using a naive approach (simply calculating every single row), one obtains a time-complexity of O(L^2).
However, the professor said, it's possible with O(L), but I simply can't find any pattern!!!
I'm not sure why this problem would be used in an advanced algorithms course, but yes, you can do this in O(l) = O(log n) time.
There are a couple ways you can do it, but they both rely on recognizing that:
For the problem statement, it doesn't matter what digits you use. Lets use 0, 1, and 2 instead of 2, 3, and 5. Then
If a and b are the input numbers and c is the output, then c = -(a+b) mod 3
You can build the whole triangle using c = a+b mod 3 instead, and then just negate every second row.
Now the two ways you can do this in O(log n) time are:
For each digit d in the input, calculate the number of times (call it k) that it gets added into the final sum, add up all the kd mod 3, and then negate the result if you started with an even number of digits. That takes constant time per digit. Alternatively:
recognize that you can do arithmetic on n-sized values in constant time. Make a value that is a bit mask of all the digits in n. That takes 2 bits each. Then by using bitwise operations you can calculate each row from the previous one in constant time, for O(log n) time altogether.
Here's an implementation of the 2nd way in python:
def niceTriangle(n):
# a vector of 3-bit integers mod 3
rowvec = 0
# a vector of 1 for each number in the row
onevec = 0
# number of rows remaining
rows = 0
# mapping for digits 0-9
digitmap = [0, 0, 0, 1, 1, 2, 2, 2, 2, 2]
# first convert n into the first row
while n > 0:
digit = digitmap[n % 10]
n = n//10
rows += 1
onevec = (onevec << 3) + 1
rowvec = (rowvec << 3) + digit
if rows%2 == 0:
# we have an even number of rows -- negate everything
rowvec = ((rowvec&onevec)<<1) | ((rowvec>>1)&onevec)
while rows > 1:
# add each number to its neighbor
rowvec += (rowvec >> 3)
# isolate the entries >= 3, by adding 1 to each number and
# getting the 2^2 bit
gt3 = ((rowvec + onevec) >> 2) & onevec
# subtract 3 from all the greater entries
rowvec -= gt3*3
rows -= 1
return [2,3,5][rowvec%4]

Get permutation count

I searching for an algorithm which gives me the permutation count of the elements 1....n. If i define the cycle lengths.
For example n := 4
<Set of cycle lengths> -> permutation count
1,1,1,1 -> 1 read 4 cycles of length 1 leads to 1 permutation: 1,2,3,4
1,1,2 -> 5 read 2 cycles of length 1 and 1 cycle of length 2 leads to 5 permutations: 1,2,4,3, 1,4,3,2, 1,3,2,4, 2,1,3,4, 3,2,1,4,
2,2 -> 3 read 2 cycles of length 2 leads to 3 permutations: 2,1,4,3, 3,4,1,2,4,3,2,1
1,3 -> 9 read 1 cycle of length 1 and 1 cycle of length 3 leads to 9 permutations 1,3,2,4, 1,3,4,2, 1,4,2,3, 2,3,1,4, 2,4,3,1, 3,1,2,4, 3,2,4,1,4,1,3,2, 4,2,1,3,
4 -> 6 read 1 cycle of length 4 leads to 6 permutations:
2,3,4,1, 2,4,1,3, 3,1,4,2, 3,4,2,1, 4,1,2,3, 4,3,1,2
How can i compute the permutation count of a given set consisting cycle lengths? Iterating through all permutations is not an option.
For a given cycle type, we can produce a permutation with that cycle type by writing down a permutation of the list 1, ..., n and then bracketing it appropriately, according to the lengths in the cycle type, to get a permutation written in cycle notation.
For example, if we want cycle type (3, 2, 2), then the permutation 1, 2, 3, 4, 5, 6, 7 is bracketed as (1 2 3)(4 5)(6 7), while 5, 1, 6, 2, 4, 3, 7 gives (5 1 6)(2 4)(3 7).
It's clear that we get all permutations of cycle type (3, 2, 2) this way, but it's also clear that we can get each permutation in multiple different ways. There are two causes of overcounting: first, we can make a cyclic shift for any of the cycles: (5 1 6)(2 4)(3 7) is the same permutation as (1 6 5)(2 4)(3 7) or (6 5 1)(2 4)(3 7). Second, cycles of the same length can be permuted arbitrarily: (5 1 6)(2 4)(3 7) is the same permutation as (5 1 6)(3 7)(2 4). A bit of thought should convince you that these are the only possible causes of overcounting.
To account for both causes of overcounting, we divide the total number of permutations by (a) the product of the cycle lengths, and also (b) the factorial of the number of cycles for any given cycle length. In the (3, 2, 2) case: we divide by 3 × 2 × 2 for (a), and 2! for (b), because there are two cycles of length 2.
Since this is Stack Overflow, here's some Python code:
from collections import Counter
from math import factorial
def count_cycle_type(p):
"""Number of permutations with a given cycle type."""
count = factorial(sum(p))
for cycle_length, ncycles in Counter(p).items():
count //= cycle_length ** ncycles * factorial(ncycles)
return count
Example:
>>> count_cycle_type((2, 2))
3
>>> count_cycle_type((3, 2, 2))
210
To double check correctness, we can add the counts for all cycle types of a given length n, and check that we get n!. The cycle types are the partitions of n. We can compute those fairly simply by a recursive algorithm. Here's some code to do that. partitions is the function we want; bounded_partitions is a helper.
def bounded_partitions(n, k):
"""Generate partitions of n with largest element <= k."""
if k == 0:
if n == 0:
yield ()
else:
if n >= k:
for c in bounded_partitions(n - k, k):
yield (k,) + c
yield from bounded_partitions(n, k - 1)
def partitions(n):
"""Generate partitions of n."""
return bounded_partitions(n, n)
Example:
>>> for partition in partitions(5): print(partition)
...
(5,)
(4, 1)
(3, 2)
(3, 1, 1)
(2, 2, 1)
(2, 1, 1, 1)
(1, 1, 1, 1, 1)
And here's the double check: the sum of all the cycle type counts, for total lengths 5, 6, 7 and 20. We get the expected results of 5!, 6!, 7! and 20!.
>>> sum(count_cycle_type(p) for p in partitions(5))
120
>>> sum(count_cycle_type(p) for p in partitions(6))
720
>>> sum(count_cycle_type(p) for p in partitions(7))
5040
>>> sum(count_cycle_type(p) for p in partitions(20))
2432902008176640000
>>> factorial(20)
2432902008176640000
This can be broken down into:
The number of ways to partition elements in to buckets matching the required count of elements with each distinct cycle size;
Multiplied by, for each distinct cycle size, the number of unique ways to partition the elements evenly into the required number of cycles;
Multiplied by, for each cycle, the number of distinct cyclic orderings
1: For bucket sizes s1...sk, that works out to n!/(s1! * ... * sk!)
2: For a bucket containing m elements that must be partitioned into c cycles, there are m!/( (m/c)!c * c! ) ways
3: For a cycle containing m elements, there are (m-1)! distinct cyclic orderings if m > 1, and just 1 ordering otherwise

Count the total number ways to reach the nth stair using step 1, 2 or 3 but the step 3 can be taken only once

For any given value N we have to find the number of ways to reach the top while using steps of 1,2 or 3 but we can use 3 steps only once.
for example if n=7
then possible ways could be
[1,1,1,1,1,1,1]
[1,1,1,1,1,2]
etc but we cannot have [3,3,1] or [1,3,3]
I have managed to solve the general case without the constraint of using 3 only once with dynamic programming as it forms a sort of fibonacci series
def countWays(n) :
res = [0] * (n + 1)
res[0] = 1
res[1] = 1
res[2] = 2
for i in range(3, n + 1) :
res[i] = res[i - 1] + res[i - 2] + res[i - 3]
return res[n]
how do I figure out the rest of it?
Let res0[n] be the number of ways to reach n steps without using a 3-step, and let res1[n] be the number of ways to reach n steps after having used a 3-step.
res0[i] and res1[i] are easily calculated from the previous values, in a manner similar to your existing code.
This is an example of a pretty common technique that is often called "graph layering". See, for example: Shortest path in a maze with health loss
Let us first ignore the three steps here. Imagine that we can only use steps of one and two. Then that means that for a given number n. We know that we can solve this with n steps of 1 (one solution), or n-2 steps of 1 and one step of 2 (n-1 solutions); or with n-4 steps of 1 and two steps of 2, which has n-2×n-3/2 solutions, and so on.
The number of ways to do that is related to the Fibonacci sequence. It is clear that the number of ways to construct 0 is one: just the empty list []. It is furthermore clear that the number of ways to construct 1 is one as well: a list [1]. Now we can proof that the number of ways Wn to construct n is the sum of the ways Wn-1 to construct n-1 plus the number of ways Wn-2 to construct n-2. The proof is that we can add a one at the end for each way to construct n-1, and we can add 2 at the end to construct n-2. There are no other options, since otherwise we would introduce duplicates.
The number of ways Wn is thus the same as the Fibonacci number Fn+1 of n+1. We can thus implement a Fibonacci function with caching like:
cache = [0, 1, 1, 2]
def fib(n):
for i in range(len(cache), n+1):
cache.append(cache[i-2] + cache[i-1])
return cache[n]
So now how can we fix this for a given step of three? We can here use a divide and conquer method. We know that if we use a step of three, it means that we have:
1 2 1 … 1 2 3 2 1 2 2 1 2 … 1
\____ ____/ \_______ _____/
v v
sum is m sum is n-m-3
So we can iterate over m, and each time multiply the number of ways to construct the left part (fib(m+1)) and the right part (fib(n-m-3+1)) we here can range with m from 0 to n-3 (both inclusive):
def count_ways(n):
total = 0
for m in range(0, n-2):
total += fib(m+1) * fib(n-m-2)
return total + fib(n+1)
or more compact:
def count_ways(n):
return fib(n+1) + sum(fib(m+1) * fib(n-m-2) for m in range(0, n-2))
This gives us:
>>> count_ways(0) # ()
1
>>> count_ways(1) # (1)
1
>>> count_ways(2) # (2) (1 1)
2
>>> count_ways(3) # (3) (2 1) (1 2) (1 1 1)
4
>>> count_ways(4) # (3 1) (1 3) (2 2) (2 1 1) (1 2 1) (1 1 2) (1 1 1 1)
7

Dynamic programming: can interval of even 1's and 0's be found in linear time?

Found the following inteview q on the web:
You have an array of
0s and 1s and you want to output all the intervals (i, j) where the
number of 0s and numbers of 1s are equal. Example
pos = 0 1 2 3 4 5 6 7 8
0 1 0 0 1 1 1 1 0
One interval is (0, 1) because there the number
of 0 and 1 are equal. There are many other intervals, find all of them
in linear time.
I think there is no linear time algo, as there may be n^2 such intervals.
Am I right? How can I prove that there are n^2 such ?
This is the fastest way I can think of to do this, and it is linear to the number of intervals there are.
Let L be your original list of numbers and A be a hash of empty arrays where initially A[0] = [0]
sum = 0
for i in 0..n
if L[i] == 0:
sum--
A[sum].push(i)
elif L[i] == 1:
sum++
A[sum].push(i)
Now A is essentially an x y graph of the sum of the sequence (x is the index of the list, y is the sum). Every time there are two x values x1 and x2 to an y value, you have an interval (x1, x2] where the number of 0s and 1s is equal.
There are m(m-1)/2 (arithmetic sum from 1 to m - 1) intervals where the sum is 0 in every array M in A where m = M.length
Using your example to calculate A by hand we use this chart
L # 0 1 0 1 0 0 1 1 1 1 0
A keys 0 -1 0 -1 0 -1 -2 -1 0 1 2 1
L index -1 0 1 2 3 4 5 6 7 8 9 10
(I've added a # to represent the start of the list with an key of -1. Also removed all the numbers that are not 0 or 1 since they're just distractions) A will look like this:
[-2]->[5]
[-1]->[0, 2, 4, 6]
[0]->[-1, 1, 3, 7]
[1]->[8, 10]
[2]->[9]
For any M = [a1, a2, a3, ...], (ai + 1, aj) where j > i will be an interval with the same number of 0s as 1s. For example, in [-1]->[0, 2, 4, 6], the intervals are (1, 2), (1, 4), (1, 6), (3, 4), (3, 6), (5, 6).
Building the array A is O(n), but printing these intervals from A must be done in linear time to the number of intervals. In fact, that could be your proof that it is not quite possible to do this in linear time to n because it's possible to have more intervals than n and you need at least the number of interval iterations to print them all.
Unless of course you consider building A is enough to find all the intervals (since it's obvious from A what the intervals are), then it is linear to n :P
A linear solution is possible (sorry, earlier I argued that this had to be n^2) if you're careful to not actually print the results!
First, let's define a "score" for any set of zeros and ones as the number of ones minus the number of zeroes. So (0,1) has a score of 0, while (0) is -1 and (1,1) is 2.
Now, start from the right. If the right-most digit is a 0 then it can be combined with any group to the left that has a score of 1. So we need to know what groups are available to the left, indexed by score. This suggests a recursive procedure that accumulates groups with scores. The sweep process is O(n) and at each step the process has to check whether it has created a new group and extend the table of known groups. Checking for a new group is constant time (lookup in a hash table). Extending the table of known groups is also constant time (at first I thought it wasn't, but you can maintain a separate offset that avoids updating each entry in the table).
So we have a peculiar situation: each step of the process identifies a set of results of size O(n), but the calculation necessary to do this is constant time (within that step). So the process itself is still O(n) (proportional to the number of steps). Of course, actually printing the results (either during the step, or at the end) makes things O(n^2).
I'll write some Python code to test/demonstrate.
Here we go:
SCORE = [-1,1]
class Accumulator:
def __init__(self):
self.offset = 0
self.groups_to_right = {} # map from score to start index
self.even_groups = []
self.index = 0
def append(self, digit):
score = SCORE[digit]
# want existing groups at -score, to sum to zero
# but there's an offset to correct for, so we really want
# groups at -(score+offset)
corrected = -(score + self.offset)
if corrected in self.groups_to_right:
# if this were a linked list we could save a reference
# to the current value. it's not, so we need to filter
# on printing (see below)
self.even_groups.append(
(self.index, self.groups_to_right[corrected]))
# this updates all the known groups
self.offset += score
# this adds the new one, which should be at the index so that
# index + offset = score (so index = score - offset)
groups = self.groups_to_right.get(score-self.offset, [])
groups.append(self.index)
self.groups_to_right[score-self.offset] = groups
# and move on
self.index += 1
#print self.offset
#print self.groups_to_right
#print self.even_groups
#print self.index
def dump(self):
# printing the results does take longer, of course...
for (end, starts) in self.even_groups:
for start in starts:
# this discards the extra points that were added
# to the data after we added it to the results
# (avoidable with linked lists)
if start < end:
print (start, end)
#staticmethod
def run(input):
accumulator = Accumulator()
print input
for digit in input:
accumulator.append(digit)
accumulator.dump()
print
Accumulator.run([0,1,0,0,1,1,1,1,0])
And the output:
dynamic: python dynamic.py
[0, 1, 0, 0, 1, 1, 1, 1, 0]
(0, 1)
(1, 2)
(1, 4)
(3, 4)
(0, 5)
(2, 5)
(7, 8)
You might be worried that some additional processing (the filtering for start < end) is done in the dump routine that displays the results. But that's because I am working around Python's lack of linked lists (I want to both extend a list and save the previous value in constant time).
It may seem surprising that the result is of size O(n^2) while the process of finding the results is O(n), but it's easy to see how that is possible: at one "step" the process identifies a number of groups (of size O(n)) by associating the current point (self.index in append, or end in dump()) with a list of end points (self.groups_to_right[...] or ends).
Update: One further point. The table of "groups to the right" will have a "typical width" of sqrt(n) entries (this follows from the central limit theorem - it's basically a random walk in 1D). Since an entry is added at each step, the average length is also sqrt(n) (the n values shared out over sqrt(n) bins). That means that the expected time for this algorithm (ie with random inputs), if you include printing the results, is O(n^3/2) even though worst case is O(n^2)
Answering directly the question:
you have to constructing an example where there are more than O(N) matches:
let N be in the form 2^k, with the following input:
0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 (here, N=16)
number of matches (where 0 is the starting character):
length #
2 N/2
4 N/2 - 1
6 N/2 - 2
8 N/2 - 3
..
N 1
The total number of matches (starting with 0) is: (1+N/2) * (N/2) / 2 = N^2/8 + N/4
The matches starting with 1 are almost the same, expect that it is one less for each length.
Total: (N^2/8 + N/4) * 2 - N/2 = N^2/4
Every interval will contain at least one sequence of either (0,1) or (1,0). Therefore, it's simply a matter of finding every occurance of (0,1) or (1,0), then for each seeing if it is adjacent to an existing solution or if the two bookend elements form another solution.
With a bit of storage trickery you will be able to find all solutions in linear time. Enumerating them will be O(N^2), but you should be able to encode them in O(N) space.

How does this work? Weird Towers of Hanoi Solution

I was lost on the internet when I discovered this unusual, iterative solution to the towers of Hanoi:
for (int x = 1; x < (1 << nDisks); x++)
{
FromPole = (x & x-1) % 3;
ToPole = ((x | x-1) + 1) % 3;
moveDisk(FromPole, ToPole);
}
This post also has similar Delphi code in one of the answers.
However, for the life of me, I can't seem to find a good explanation for why this works.
Can anyone help me understand it?
the recursive solution to towers of Hanoi works so that if you want to move N disks from peg A to C, you first move N-1 from A to B, then you move the bottom one to C, and then you move again N-1 disks from B to C. In essence,
hanoi(from, to, spare, N):
hanoi(from, spare, to, N-1)
moveDisk(from, to)
hanoi(spare, to, from, N-1)
Clearly hanoi( _ , _ , _ , 1) takes one move, and hanoi ( _ , _ , _ , k) takes as many moves as 2 * hanoi( _ , _ , _ , k-1) + 1. So the solution length grows in the sequence 1, 3, 7, 15, ... This is the same sequence as (1 << k) - 1, which explains the length of the loop in the algorithm you posted.
If you look at the solutions themselves, for N = 1 you get
FROM TO
; hanoi(0, 2, 1, 1)
0 2 movedisk
For N = 2 you get
FROM TO
; hanoi(0, 2, 1, 2)
; hanoi(0, 1, 2, 1)
0 1 ; movedisk
0 2 ; movedisk
; hanoi(1, 2, 0, 1)
1 2 ; movedisk
And for N = 3 you get
FROM TO
; hanoi(0, 2, 1, 3)
; hanoi(0, 1, 2, 2)
; hanoi(0, 2, 1, 1)
0 2 ; movedisk
0 1 ; movedisk
; hanoi(2, 1, 0, 1)
2 1 ; movedisk
0 2 ; movedisk ***
; hanoi(1, 2, 0, 2)
; hanoi(1, 0, 2, 1)
1 0 ; movedisk
1 2 ; movedisk
; hanoi(0, 2, 1, 1)
0 2 ; movedisk
Because of the recursive nature of the solution, the FROM and TO columns follow a recursive logic: if you take the middle entry on the columns, the parts above and below are copies of each others, but with the numbers permuted. This is an obvious consequence of the algorithm itself which does not perform any arithmetics on the peg numbers but only permutes them. In the case N=4 the middle row is at x=4 (marked with three stars above).
Now the expression (X & (X-1)) unsets the least set bit of X, so it maps e.g. numbers form 1 to 7 like this:
1 -> 0
2 -> 0
3 -> 2
4 -> 0 (***)
5 -> 4 % 3 = 1
6 -> 4 % 3 = 1
7 -> 6 % 3 = 0
The trick is that because the middle row is always at an exact power of two and thus has exactly one bit set, the part after the middle row equals the part before it when you add the middle row value (4 in this case) to the rows (i.e. 4=0+4, 6=2+6). This implements the "copy" property, the addition of the middle row implements the permutation part. The expression (X | (X-1)) + 1 sets the lowest zero bit which has ones to its right, and clears these ones, so it has similar properties as expected:
1 -> 2
2 -> 4 % 3 = 1
3 -> 4 % 3 = 1
4 -> 8 (***) % 3 = 2
5 -> 6 % 3 = 0
6 -> 8 % 3 = 2
7 -> 8 % 3 = 2
As to why these sequences actually produce the correct peg numbers, let's consider the FROM column. The recursive solution starts with hanoi(0, 2, 1, N), so at the middle row (2 ** (N-1)) you must have movedisk(0, 2). Now by the recursion rule, at (2 ** (N-2)) you need to have movedisk(0, 1) and at (2 ** (N-1)) + 2 ** (N-2) movedisk (1, 2). This creates the "0,0,1" pattern for the from pegs which is visible with different permutations in the table above (check rows 2, 4 and 6 for 0,0,1 and rows 1, 2, 3 for 0,0,2, and rows 5, 6, 7 for 1,1,0, all permuted versions of the same pattern).
Now then of all the functions that have this property that they create copies of themselves around powers of two but with offsets, the authors have selected those that produce modulo 3 the correct permutations. This isn't an overtly difficult task because there are only 6 possible permutations of the three integers 0..2 and the permutations progress in a logical order in the algorithm. (X|(X-1))+1 is not necessarily deeply linked with the Hanoi problem or it doesn't need to be; it's enough that it has the copying property and that it happens to produce the correct permutations in the correct order.
antti.huima's solution is essentially correct, but I wanted something more rigorous, and it was too big to fit in a comment. Here goes:
First note: at the middle step x = 2N-1 of this algorithm, the "from" peg is 0, and the "to" peg is 2N % 3. This leaves 2(N-1) % 3 for the "spare" peg.
This is also true for the last step of the algorithm, so we see that actually the authors' algorithm
is a slight "cheat": they're moving the disks from peg 0 to peg 2N % 3, rather than a fixed,
pre-specified "to" peg. This could be changed with not much work.
The original Hanoi algorithm is:
hanoi(from, to, spare, N):
hanoi(from, spare, to, N-1)
move(from, to)
hanoi(spare, to, from, N-1)
Plugging in "from" = 0, "to" = 2N % 3, "spare" = 2N-1 % 3, we get (suppressing the %3's):
hanoi(0, 2**N, 2**(N-1), N):
(a) hanoi(0, 2**(N-1), 2**N, N-1)
(b) move(0, 2**N)
(c) hanoi(2**(N-1), 2**N, 0, N-1)
The fundamental observation here is:
In line (c), the pegs are exactly the pegs of hanoi(0, 2N-1, 2N, N-1) shifted by 2N-1 % 3, i.e.
they are exactly the pegs of line (a) with this amount added to them.
I claim that it follows that when we
run line (c), the "from" and "to" pegs are the corresponding pegs of line (a) shifted by 2N-1 % 3. This
follows from the easy, more general lemma that in hanoi(a+x, b+x, c+x, N), the "from and "to" pegs are shifted exactly x from in hanoi(a, b, c, N).
Now consider the functions
f(x) = (x & (x-1)) % 3
g(x) = (x | (x-1)) + 1 % 3
To prove that the given algorithm works, we only have to show that:
f(2N-1) == 0 and g(2N-1) == 2N
for 0 < i < 2N, we have f(2N - i) == f(2N + i) + 2N % 3, and g(2N - i) == g(2N + i) + 2N % 3.
Both of these are easy to show.
This isn't directly answering the question, but it was too long to put in a comment.
I had always done this by analyzing the size of disk you should move next. If you look at the disks moved, it comes out to:
1 disk : 1
2 disks : 1 2 1
3 disks : 1 2 1 3 1 2 1
4 disks : 1 2 1 3 1 2 1 4 1 2 1 3 1 2 1
Odd sizes always move in the opposite direction of even ones, in order if pegs (0, 1, 2, repeat) or (2, 1, 0, repeat).
If you take a look at the pattern, the ring to move is the highest bit set of the xor of the number of moves and the number of moves + 1.

Resources