Oblong Spiral Puzzle

Brian Jones posted a solution to the Oblong Spiral Puzzle recently. I don’t normally mess with puzzles, but after looking at his solution I thought I would take a stab at it and see if I could come up with a solution in Python that was a little less complicated.

The Puzzle

The complete definition of the puzzle is on the Code Golf site. The challenge is to fill in an n x m array with a sequence of numbers starting at the upper left, and working around the outside, spiraling inwards. For example:

$ python spiral.py 3 3
  1   2   3
  8   9   4
  7   6   5

$ python spiral.py 4 5
  1   2   3   4
 14  15  16   5
 13  20  17   6
 12  19  18   7
 11  10   9   8

The actual contest applies a few other rules about submission formats, but I don’t plan to enter so I haven’t paid close attention to them.

Solution

One way to solve the puzzle is to work out an algorithm for determining the next row and column index for each element in the list of values, then iterate over the list and put each value into the right cell in the array. I chose to look at it the other way around, and iterate over the array, picking the proper value to go into each cell.

The solution starts by creating lists containing the numerical indexes of the rows and columns in the output, based on arguments given on the command line.

import itertools
import sys

if len(sys.argv) != 3:
    print 'Usage: %s <num_cols> <num_rows>' % sys.argv[0]
    sys.exit(1)
num_cols = int(sys.argv[1])
num_rows = int(sys.argv[2])

rows = range(num_rows)
cols = range(num_cols)

The next step is to initialize the array. I used a list comprehension to create a list of lists based on the row size.

# Initialize the array
spiral = [ [] * num_cols for row in xrange(num_rows) ]

As process the rows and columns are processed, I will want to know the “next” number to put in each cell. I use itertools.count() for that, skipping over the value `` since that is not supposed to be used.

# Establish a counter
n = itertools.count()
n.next() # skip zero

To fill in the array, I use a loop that fills in one row and one column each time around. After a row is completed, the remaining column indexes are reversed so that the next row is filled in going the other direction. The same operation is applied to the row indexes at the end of a column.

while rows and cols:

    # Fill in cells across the current row
    r = rows.pop()
    for c in cols:
        spiral[r][c] = n.next()

    # Walk the next row in the other direction
    cols.reverse()

    # Fill in cells along the current column
    c = cols.pop()
    for r in rows:
        spiral[r][c] = n.next()

    # Walk the next column in the other direction
    rows.reverse()

And finally the results are printed:

# Show the results
for r in xrange(num_rows):
    for c in xrange(num_cols):
        print '%3d' % spiral[r][c],
    print

The same effect could be achieved by computing the cell position each time through the loop, keeping track of when to add and when to subtract for each of the row and column counters. But by using list.reverse(), I avoid the computation and testing to see if the end of the column or row has been reached, and I think the resulting algorithm is simpler to follow. Using itertools.count() instead of incrementing an integer variable myself was just a final touch to see if I could eliminate all arithmetic from the solution.

Without Reversing the Lists

Doug Napoleone pointed out in comments that my original solution processed the data in the rows and cols lists every time reverse() was called. Unfolding the outer loop one more time so it makes a complete circuit of the spiral lets me use a reverse-iterator instead (see PEP 322). I also changed the way the counter is initialized, based on a comment from Christian Oudard. Here’s the new version:

n = itertools.count(1)

while rows or cols:

    # Row left to right
    if rows:
        r = rows.pop()
        for c in cols:
            spiral[r][c] = n.next()

    # Column top to bottom
    if cols:
        c = cols.pop(-1)
        for r in rows:
            spiral[r][c] = n.next()

    # Row right to left
    if rows:
        r = rows.pop(-1)
        for c in reversed(cols):
            spiral[r][c] = n.next()

    # Column bottom to top
    if cols:
        c = cols.pop()
        for r in reversed(rows):
            spiral[r][c] = n.next()

Sample Output

Here are a few sample runs:

$ python spiral2.py 10 4
  1   2   3   4   5   6   7   8   9  10
 24  25  26  27  28  29  30  31  32  11
 23  40  39  38  37  36  35  34  33  12
 22  21  20  19  18  17  16  15  14  13

$ python spiral2.py 15 15
  1   2   3   4   5   6   7   8   9  10  11  12  13  14  15
 56  57  58  59  60  61  62  63  64  65  66  67  68  69  16
 55 104 105 106 107 108 109 110 111 112 113 114 115  70  17
 54 103 144 145 146 147 148 149 150 151 152 153 116  71  18
 53 102 143 176 177 178 179 180 181 182 183 154 117  72  19
 52 101 142 175 200 201 202 203 204 205 184 155 118  73  20
 51 100 141 174 199 216 217 218 219 206 185 156 119  74  21
 50  99 140 173 198 215 224 225 220 207 186 157 120  75  22
 49  98 139 172 197 214 223 222 221 208 187 158 121  76  23
 48  97 138 171 196 213 212 211 210 209 188 159 122  77  24
 47  96 137 170 195 194 193 192 191 190 189 160 123  78  25
 46  95 136 169 168 167 166 165 164 163 162 161 124  79  26
 45  94 135 134 133 132 131 130 129 128 127 126 125  80  27
 44  93  92  91  90  89  88  87  86  85  84  83  82  81  28
 43  42  41  40  39  38  37  36  35  34  33  32  31  30  29

$ python spiral2.py 3 9
  1   2   3
 20  21   4
 19  22   5
 18  23   6
 17  24   7
 16  25   8
 15  26   9
 14  27  10
 13  12  11

See also