Notes on an Algorithm to Enumerate Langford Sequences

© John E. Miller, 1997.


The trick is shifting all the pairs around in a systematic order to exhaust all possible positional combinations, finding all the possible langford sequences for a given number of pairs in the process.

	123456   Actions
	......   empty arrangement
	3...3.   3's placed in first position
	.2..2.   2's can't fit in second position, conflicts with 3's
	..2..2   2's do fit in third position
	1.1...   1's can't fit in first position, conflicts with 2's
	.1.1..   1's fit in second
	312132   bingo! complete arrangement

A Hand-Waving Explanation: The algorithm proceeds simply by starting each pair at the leftmost position and scanning for the first position where it will fit. After the pair has been fit into the arrangement and all smaller pairs have likewise been attempted and removed, the pair moves over to the next available place - not back at the left. When a pair has gone all the way over to the right (i.e. the right element of the pair would fall off the end of the arrangement), attention shifts up to the next higher pair, and attention will come back down again if that higher pair is fit into the arrangement. The remainder of this page goes into various aspects of implementing this algorithm.

An note on efficiency - using a linked list

The main thing that I missed implementing this algorithm for speed was to keep a list (a linked-list in CS parlance) of unfilled positions in the arrangement. This may seem like more work in the beginning or when the arrangement is sparsely filled. This is not the case! You need to scan for them anyway, AND when the arrangement is nearly filled, having a linked list is very handy for placing the 4's, 3's, and so on. Get the idea? This is one way Rick Groth's algorithm managed to start way later than mine and overtake it computing L(2,19). (Another way is by breaking the problem down by position of the 19's, and using a computer for each position.)

I do have some of Rick's source code, but I leave that as an exercise for the reader. I don't know if any of Ruskey's students employed this time-saver. I would assume so - jm 8/2001

An Array-Based, Non-Recursive Algorithm

Notes on the above algorithm

Termination upon reaching the reflection point

The above algorithm if left alone will produce twice the number of solutions, since it continues to shift the n's all the way across the arrangement. The solutions produced are unique up to the point where the solution produced is the reversal of the previous arrangement. Solutions produced past the reflection point are mirror images of successively older solutions, to where the last solution produced is a mirror of the first.

For odd n's, this occurs exactly when the highest pair is shifted past the middle of the arrangement. For even values of n, the highest pair can occupy the middle of the arrangement, and therefore the reversal (reflection) occurs with the (n-1)st pair is shifted past the middle.

The exact positions can be known ahead of time and tested for. My algorithm just saves a copy of the previously found solution to be used in testing for this reflection point. It takes very little overhead since the test for the reflection fails quickly except when it is for real. I think this is more elegant since it works for both odd & even values of n.

Use of a position vector

In order to aid in removing a given pair from the arrangement when the time comes, the position of the leftmost element can be recorded in a vector (array). The value of p can be fetched from the vector, and the corresponding elements of A can be zeroed.

The position is also needed in order to continue the scan from the place where the pair was resting.

Erasing the 1,2,3's

Once a solution is found, there is no point in continuing to scan for a new place for the 1's, since there is only 2 places vacant in the arrangement, and the 1's were just there!

Similarly, it is easy to show that removing the 1's and the 2's does not create any new possibilities for placement of the 1's and 2's.

The same goes for removing the 3's too, except in the case where a 312132 subsequence is embedded in the larger sequence. It can replaced with its reverse in order to create yet another solution. Special programming for this situation is not warranted - it takes very little time to just let the algorithm work toward the next solution or work its way back up into higher pairs in search of the next solution.

I did some investigation of this in 1970 or so (while at WSU) and found that it made virtually no difference in the overall performance. (I was designing a special circuit at the time, and I wanted to see what if it was important or not).

But it is easy enough in the coding to remove the 1's, 2's and 3's, and let the algorithm proceed scanning for a place for the 3's. If it fails, it will go up to the 4's, etc. If it succeeds, it may lead to the reversal of the 3-subsequence. So, my programs employ this little trick.

Going from low to high

The obvious question is - what if I start by placing the 1's into the solution first and then the 2's, et cetera, up to the n's?

The answer isn't obvious to me. I don't recall making any quantitative measure of this. Intuitively, it seems easier to fit the smaller pairs in towards the last, but I really don't think they are any easier.

Perhaps someone could do some comparisons on the amount of back-tracking that has to be done between low-to-high and high-to-low algorithms. I would be surprised if there were a significant advantage between these two. Are they the same? If not, one has to be better -- but by how much? And is one consistently better? If not, which should you use based on n?

Notes on other approaches, good and bad

bits vs bytes

Use n binary masks where two bits are separated by k bits, k=1,n. Use logical operations between the arrangement word and each of the masks which you shift to the right as you scan.

Andrew Burke's program, and others over time have used this method. It is faster by a factor of 4 or so from a byte-oriented program, but is limited to the word length of the machine -- 32 bits could handle n=16, or 64 bits could handle 32 pairs (in the case of a CRAY or DEC Alpha).

The algorithm could be fine-tuned by coding in assembly language for sure, taking advantage of the particular architecture. My earlier assault on 12 was done in assembly on a 16-bit computer, but not using bit masks.

Pruning the search?

It is questionable whether there is much checking you can to at any point that is worth the overhead spent.

Stopping after each insertion to check whether it is feasible that all pairs might individually fit into the arrangement is a lot of work, but it would prevent a lot of needless work. I did a test on this, and it was less important as n increased. I.e., When there are lots of empty spaces, most pairs look feasible at some place in the arrangement. When there are fewer empty places, it pays to just let the algorithm proceed and eliminate what doesn't work.

I can't think of a reason or criteria that would define a level when such look-ahead's could be turned on or off.

In short, I see no opportunity for pruning the search. Suggestions welcome!

Recursion

The algorithm can be coded somewhat more simply in a recursive manner - but at the expense of system overhead. I see no advantage to recursion, other than a means to employ multiple processors.

Parallelism

Langford's Problem is a natural candidate for multi-processing. It can easily be decomposed into any number of independent sub-problems.

One method would be to have a top-level program generate a set of partially completed Langford sequences. The depth of this completion could be increased until you got a desired number of partial solutions, which you would save. Then using a system such as Linda, place the list of partial solutions into its Tuple Space and make them available to a pool of processors in your Linda O/S. Processors would pick up a tuple, and return the result. If a processor crashed or otherwise never returned a particular tuple, Linda would eventually assign it to another processor.

Pseudo Code

 begin
   initialize;
    repeat
     try;
     if successful
      then lowerpair
      else higherpair
    until done
 end.

Download a C program (only 2K) and see for yourself.

This is all I have time for now. I have notes from Randy Austin about some of the questions that I've raised above about testing whether it makes any difference wo work from low to high or VV.


Created by: john@timehaven•us
Modified: May 30, 2004, edited Jan 2020!