\datethis
@*Introduction. This program finds a minimum-move solution to the famous
``15 puzzle,'' using a method introduced by Richard E. Korf
[{\sl Artificial Intelligence\/ \bf27} (1985), 97--109]. It's the
first (well, really the zeroth)
of a series of ever-more-efficient ways to do the job.
My main reason for writing this group of routines was to experiment
with a new (for me) style of programming, explained in
{\mc 15PUZZLE-KORF1}.
The initial position is specified on the command line as a permutation of the
hexadecimal digits
$\{\.0,\.1,\.2,\allowbreak\.3,\allowbreak\.4,\.5,\.6,\.7,\.8,\.9,\allowbreak
\.a,\.b,\.c,\.d,\.e,\.f\}$;
this permutation is used to fill the rows of a $4\times4$ matrix, from top to
bottom and left to right. For example, `\.{159d26ae37bf48c0}' specifies the
starting position
$$\vcenter{\halign{&\enspace\tt#\cr
1&5&9&d\cr
2&6&a&e\cr
3&7&b&f\cr
4&8&c&0\cr
}}$$
The number \.0 stands for a blank cell. Each step in solving the puzzle
consists of swapping \.0 with one of its neighbors. The goal position
is always
$$\vcenter{\halign{&\enspace\tt#\cr
1&2&3&4\cr
5&6&7&8\cr
9&a&b&c\cr
d&e&f&0\cr
}}$$
(Korf had a different goal position, namely \.{0123456789abcdef}. I agree
that his convention is mathematically superior to mine; but it conflicts
with a 125-year-old tradition. So I have retained the historic practice.
One can of course interchange our conventions, if desired, by
rotating the board by $180^\circ$ and by replacing each nonzero digit
$x$ by $16-x$.)
@c
#include
#include
char board[16];
char start[16];
short tim[100];
char dir[100], pos[100];
int timer;
main(int argc,char *argv[])
{
register int j,k,s,t,l,d,p,q,del,piece,moves;
@ ;
@;
@;
}
@ Let's regard the 16 cell positions as two-digit numbers in quaternary
(radix-4) notation:
$$\vcenter{\halign{&\enspace#\cr
00&01&02&03\cr
10&11&12&13\cr
20&21&22&23\cr
30&31&32&33\cr
}}$$
Thus each cell is identified by its row number $r$ and its column number $c$,
making a two-nyp code $(r,c)$, with $0\le r,c<4$.
Furthermore, it's convenient to renumber the input digits \.1, \.2,
\dots,~\.f, so that they match their final destination positions,
00, 01, \dots,~32. This conversion simply subtracts~1; so \.0 gets
mapped into $-1$. The example initial position given earlier
will therefore appear as follows in the |start| array:
$$\vcenter{\halign{&\enspace$\hfil#\hfil$\cr
00&10&20&30\cr
01&11&21&31\cr
02&12&22&32\cr
03&13&23&-1\cr
}}$$
Half of the initial positions make the puzzle unsolvable, because the
permutation must be odd if and only if the \.0 must move an odd number of
times. This solvability condition is checked when we read the input.
@d row(x) ((x)>>2)
@d col(x) ((x)&0x3)
@ =
if (argc!=2) {
fprintf(stderr,"Usage: %s startposition\n",argv[0]);@+
exit(-1);
}
for (j=0;k=argv[1][j];j++) {
if (k>='0' && k<='9') k-='0';
else if (k>='a' && k<='f') k-='a'-10;
else {
fprintf(stderr,
"The start position should use only hex digits (0123456789abcdef)!\n");
exit(-2);
}
if (start[k]) {
fprintf(stderr,"Your start position uses %x twice!\n",k);@+
exit(-3);
}
start[k]=1;
}
for (k=0;k<16;k++) if (start[k]==0) {
fprintf(stderr,"Your start position doesn't use %x!\n",k);@+
exit(-4);
}
for (del=j=0;k=argv[1][j];j++) {
if (k>='0' && k<='9') k-='0';@+else k-='a'-10;
start[j]=k-1;
for (s=0;sstart[j]) del++; /* count inversions */
if (k==0) t=j;
}
if (((row(t)+col(t)+del)&0x1)==0) {
printf("Sorry ... the goal is unreachable from that start position!\n");
exit(0);
}
@* Korf's method.
If piece $(r,c)$ is currently in board position $(r',c')$, it must make
at least $\vert r-r'\vert+\vert c-c'\vert$ moves before it reaches home.
The sum of these numbers, over all fifteen pieces, is called~$h$;
this quantity is also known as the ``Manhattan metric lower bound.''
We will say that a move is {\it happy\/} if it moves a piece closer
to the goal; otherwise we'll call the move {\it sad}.
Korf's key idea is to try first to find a solution that makes only happy
moves. (For example, one can actually win from the starting position
\.{bc9e80df3412a756} by making $h=56$ moves that are entirely happy.)
If that fails, we start over, but this time we try to make $h+1$ happy moves
and 1~sad move. And if that also fails, we try for $h+k$ happy moves
and $k$ sad ones, for $k=2$, 3, \dots, until finally we succeed.
That strategy may sound silly, because each new value of~$k$ repeats
calculations already made. But it's actually brilliant, for two reasons:
(1)~The search can be carried out with almost no memory, for any
fixed value of~$k$ --- in fact, this program uses fewer than 2000
bytes for all its data. (2)~The total running time for $k=0$, 1, \dots,~$k_0$
isn't much more than the time needed for a {\it single\/} run with
$k=k_0$, because the running time increases exponentially with~$k$.
Memory requirements are minimal because we can get away with memoryless
depth-first search to explore all solutions. The fifteen puzzle is
much nicer in this respect than many other problems; indeed, a blind
depth-first search as used here is often a poor choice in other applications,
because it might explore many subproblems repeatedly, having no inkling
that it has already ``been there and done that.'' But the search tree
of the fifteen puzzle has comparatively few overlapping branches.
For example, the shortest cycle of moves that restores a previously examined
position occurs only when we go thrice around a $2\times2$ subsquare,
leading to a cycle of length 12; therefore the first five levels below any
node of the tree consist entirely of distinct states of the board,
and only a few duplicates occur at level six.
Note: The Manhattan metric is somewhat weak, and substantially better
lower bounds are known. Some day I plan to incorporate them into
later programs in this series. The simple approach adopted here
is sufficient to handle most random initial positions in a reasonable
amount of time. But when it is applied to the example of transposition,
in the introduction, it is too slow; that example has a Manhattan
lower bound of 40, yet the shortest solutions have 72 moves. Thus
the $k$ value for that problem is 16 \dots\ and each new value of~$k$
takes empirically about 5.7 times as long as the previous case.
@ Saving memory means that the computation lives entirely within the
computer's high-speed cache. Furthermore, we can optimize the
inner loop, as shown in the {\mc 15PUZZLE-KORF1}, the next
program in this series. But here I'm using the most straightforward
implementation, so that it will be possible to test if more
sophisticated ideas actually do save time on a real machine.
@=
@;
if (moves==0) goto win; /* otherwise our solution will take $6+6$ moves! */
while (1) {
timer=time(0);
t=moves;
/* desired number of |(@[@t(sad moves)@>@]<<8)+@[@t(happy moves)@>@]| */
@;
printf(" ... no solution with %d+%d moves (%d sec)\n",
moves&0xff,moves>>8,time(0)-timer);
moves+=0x101; /* add a sad move and a happy move to the current quota */
}
win:@;
@ @=
for (j=moves=0;j<16;j++) if (start[j]>=0) {
del=row(start[j])-row(j);
moves+=(del<0? -del: del);
del=col(start[j])-col(j);
moves+=(del<0? -del: del);
}
@ The main control routine is typical for backtracking.
Directions are encoded as follows: $\rm east=0$, $\rm north=1$,
$\rm west=2$, $\rm south=3$. (Think of powers of~$i$ in the complex
plane.)
When a move is legal, we set |del=0x1| if the move is happy,
|del=0x100| if the move is sad. Then |t| becomes |t-del| after
the move has been made; but we can't make the move if |t~~=
for (j=0;j<16;j++) {
board[j]=start[j];
if (board[j]<0) p=j;
}
pos[0]=16, l=1;
newlevel: d=0, tim[l]=t, pos[l]=p, q=pos[l-1];
trymove:@+switch(d) {
case 0:@+ if (col(p)<=2 && q!=p+1) @~~;
d++; /* fall through to next case */
case 1:@+ if (row(p)>=1 && q!=p-4) @;
d++; /* fall through to next case */
case 2:@+ if (col(p)>=1 && q!=p-1) @;
d++; /* fall through to next case */
case 3:@+ if (row(p)<=2 && q!=p+4) @;
d++; /* fall through to next case */
case 4: goto backtrack;
}
/* at this point |q| is the new empty cell position */
/* and |del| has been set to |0x1| or |0x100| as described above */
if (t<=del) {
if (t==del) goto win;
d++;@+ goto trymove;
}
dir[l]=d, board[p]=board[q], t-=del, p=q, l++;
goto newlevel;
backtrack:@+if (l>1) {
l--;
q=pos[l], board[p]=board[q], p=q, q=pos[l-1], t=tim[l], d=dir[l]+1;
goto trymove;
}
@ We're going to move the {\it empty\/} cell east, by moving |piece| west.
@=
{
q=p+1, piece=board[q];
del=(col(piece) =
{
q=p-4, piece=board[q];
del=(row(piece)>row(q)? 0x1: 0x100);
break;
}
@ @=
{
q=p-1, piece=board[q];
del=(col(piece)>col(q)? 0x1: 0x100);
break;
}
@ @=
{
q=p+4, piece=board[q];
del=(row(piece)=
pos[l+1]=q;
printf("Solution in %d+%d moves: ",moves&0xff,moves>>8);
if (moves>0) {
for (j=0;j<16;j++) board[j]=start[j];
for (k=1;k<=l;k++) {
printf("%x",board[pos[k+1]]+1);
board[pos[k]]=board[pos[k+1]];
}
printf(" (%d sec)\n",time(0)-timer);
}
@*Index.