\def\mod{\mathop{mod}}
@s vector template
@s map template
@s void int
@* Introduction.
This simple class represents positions of a 3x3x3 Rubik's cube
efficiently (both in space and time). It is the basis of almost every
program I have written for the cube over the past few years.
@ Preface of header file.
We use a common technique to protect against multiple inclusion and
thus redeclaration, by starting the main include file with a
conditional that checks whether the file has already been included.
We then include the standard headers that we need. We also flatten
the namespace so vector and other components are in our namespace
for convenience.
@(cubepos.h@>=
#ifndef CUBEPOS_H
#define CUBEPOS_H
#include
#include
#include
#include
#include
#include
using namespace std ;
@ Distance metric.
Early in computer cubing history, two primary move metrics were
defined. The {\it half-turn metric} counts every quarter turn and
every half turn of a face as a single move; this was adopted by most
west-coast researchers. The {\it quarter-turn metric} counts a half
turn as two moves; this was adopted by east-coast researchers. The
quarter-turn metric has some nice properties; for instance, odd
positions are exactly those reachable by an odd number of moves.
Nonetheless, the half-turn metric is the more common metric,
corresponding more closely to what most people think of as a ``move''.
This class supports only the half turn metric at the moment.
@ Common constants.
The following constants are used throughout the class and by other
programs that use this. The |M| constant is the number of
automorphisms of the cube induced by rotation and reflection; these
automorphisms themselves form a group, commonly called $M$.
@(cubepos.h@>=
const int NMOVES = 18 ;
const int TWISTS = 3 ;
const int FACES = 6 ;
const int M = 48 ;
const int CUBIES = 24 ;
@ Class declaration.
Before the class, we declare a public shared copy of the solved cube
(the identity of the group); for convenience, we do not make it a
static member object but instead make it a global. Then we define the
class. We break it into four components for convenience. Note that
we keep the data representation public; we trust users of this class
not to abuse this privilege. Many things are simplified by direct
access to the data. We also provide a slot for general utility
functions, like error reporting, time reporting, and random number
generation.
@s cubepos int
@s moveseq int
@(cubepos.h@>=
extern const class cubepos identity_cube ; @/
@ @;
class cubepos {
public: @/
@ @;
@ @;
@ @;
} ;
@ @;
#endif
@* Representation and numbering.
The Rubik's cube has twenty movable cubies that move around the six
face centers. If we do not consider rotations of the whole cube,
those twenty cubies are the only pieces that move. Whole-cube
rotations are not generally considered ``moves'' so we will ignore
these for the moment.
@ Representing the corners.
We represent the corners and edges separately. Let us start with
the corners. We number the corners 0 to 7 starting with the top
layer; the back left corner is 0, then the back right corner is
1, then the front left corner is 2, and the front right corner
is 3. We number the corners on the bottom layer in the same order,
assigning them 4, 5, 6, and 7.
\vskip\baselineskip
\hbox to \hsize{\hfil
\vbox{\halign{
\strut\vrule height 15pt depth 7pt
\hbox to 20pt{\hfil#\hfil}\vrule&\hbox to 20pt{\hfil#\hfil}\vrule
&\hbox to 20pt{\hfil#\hfil}\vrule\cr
\noalign{\hrule}
0&&1\cr
\noalign{\hrule}
&&\cr
\noalign{\hrule}
2&&3\cr
\noalign{\hrule}
\omit\span\omit\span\omit\vbox to 16pt{}\hfil Top Layer\hfil\cr
}}
\hskip 40pt
\vbox{\halign{
\strut\vrule height 15pt depth 7pt
\hbox to 20pt{\hfil#\hfil}\vrule&\hbox to 20pt{\hfil#\hfil}\vrule
&\hbox to 20pt{\hfil#\hfil}\vrule\cr
\noalign{\hrule}
&&\cr
\noalign{\hrule}
&&\cr
\noalign{\hrule}
&&\cr
\noalign{\hrule}
\omit\span\omit\span\omit\vbox to 16pt{}\hfil Middle Layer\hfil\cr
}}
\hskip 40pt
\vbox{\halign{
\strut\vrule height 15pt depth 7pt
\hbox to 20pt{\hfil#\hfil}\vrule&\hbox to 20pt{\hfil#\hfil}\vrule
&\hbox to 20pt{\hfil#\hfil}\vrule\cr
\noalign{\hrule}
4&&5\cr
\noalign{\hrule}
&&\cr
\noalign{\hrule}
6&&7\cr
\noalign{\hrule}
\omit\span\omit\span\omit\vbox to 16pt{}\hfil Bottom Layer\hfil\cr
}}\hfil}
Each cubie can be in one of the eight slots, and it can have up to
three distinct orientations, for a total of 24 distinct states (this
is |CUBIES|). We therefore use an |unsigned char| to hold these
values. The low three bits of the value in |c[i]| indicates the slot
where corner cube |i| is. The next two bits give the corner twist of
that cubie; 0 indicates no twist, 1 indicates a clockwise twist, and 2
indicates a counterclockwise twist. We will define our corner twist
convention later.
@=
unsigned char c[8] ;
@ Representing the edge cubies.
The edges are represented in a similar fashion to the corners.
There are twelve edges.
\vskip\baselineskip
\hbox to \hsize{\hfil
\vbox{\halign{
\strut\vrule height 15pt depth 7pt
\hbox to 20pt{\hfil#\hfil}\vrule&\hbox to 20pt{\hfil#\hfil}\vrule
&\hbox to 20pt{\hfil#\hfil}\vrule\cr
\noalign{\hrule}
&0&\cr
\noalign{\hrule}
1&&2\cr
\noalign{\hrule}
&3&\cr
\noalign{\hrule}
\omit\span\omit\span\omit\vbox to 16pt{}\hfil Top Layer\hfil\cr
}}
\hskip 40pt
\vbox{\halign{
\strut\vrule height 15pt depth 7pt
\hbox to 20pt{\hfil#\hfil}\vrule&\hbox to 20pt{\hfil#\hfil}\vrule
&\hbox to 20pt{\hfil#\hfil}\vrule\cr
\noalign{\hrule}
4&&5\cr
\noalign{\hrule}
&&\cr
\noalign{\hrule}
6&&7\cr
\noalign{\hrule}
\omit\span\omit\span\omit\vbox to 16pt{}\hfil Middle Layer\hfil\cr
}}
\hskip 40pt
\vbox{\halign{
\strut\vrule height 15pt depth 7pt
\hbox to 20pt{\hfil#\hfil}\vrule&\hbox to 20pt{\hfil#\hfil}\vrule
&\hbox to 20pt{\hfil#\hfil}\vrule\cr
\noalign{\hrule}
&8&\cr
\noalign{\hrule}
9&&10\cr
\noalign{\hrule}
&11&\cr
\noalign{\hrule}
\omit\span\omit\span\omit\vbox to 16pt{}\hfil Bottom Layer\hfil\cr
}}\hfil}
Each edge can be in one of twelve slots, and it can have only two
orientations, for a total again of 24 possible states. The low bit of
the value in |e[i]| indicates whether edge |i| is flipped or not; a 0
indicates it is not, and a 1 indicates it is. The next four bits
indicate the edge slot that edge |i| currently resides in. We will
define our edge flip convention later.
@=
unsigned char e[12] ;
@ We need methods to compare two |cubepos| objects for equality and
for total ordering. We use |memcmp| to do the work; the |g++|
compiler (at least) uses efficient intrinsics for this.
@=
inline bool operator<(const cubepos &cp) const {
return memcmp(this, &cp, sizeof(cp)) < 0 ;
}
inline bool operator==(const cubepos &cp) const {
return memcmp(this, &cp, sizeof(cp))==0 ;
}
inline bool operator!=(const cubepos &cp) const {
return memcmp(this, &cp, sizeof(cp))!=0 ;
}
@ Convenience methods and arrays.
We provide these four inline functions to make it convenient to go to
and from separate permutation/orientation to combined cubie values,
and from cubie values to permutations and orientations. We also provide
fast routines that lets us add the orientation (only) from one
cubieval to another cubieval (for both corners and edges).
Finally, we throw in the initialization declaration.
@=
static inline int edge_perm(int cubieval) { return cubieval >> 1 ; }
static inline int edge_ori(int cubieval) { return cubieval & 1 ; }
static inline int corner_perm(int cubieval) { return cubieval & 7 ; }
static inline int corner_ori(int cubieval) { return cubieval >> 3 ; }
static inline int edge_flip(int cubieval) { return cubieval ^ 1 ; }
static inline int edge_val(int perm, int ori) { return perm * 2 + ori ; }
static inline int corner_val(int perm, int ori) { return ori * 8 + perm ; }
static inline int edge_ori_add(int cv1, int cv2) { return cv1 ^ edge_ori(cv2) ; }
static inline int corner_ori_add(int cv1, int cv2)
{ return mod24[cv1 + (cv2 & 0x18)] ; }
static inline int corner_ori_sub(int cv1, int cv2)
{ return cv1 + corner_ori_neg_strip[cv2] ; }
static void init() ;
@ We need to put the static data into the C++ file; it's rather
annoying that we need to both declare and define these arrays. We
also provide an initialization routine that follows the declarations
and any other utility functions we need.
@=
#include
#include "cubepos.h"
#include
@ @;
@ @;
void cubepos::init() {
static int initialized = 0 ;
if (initialized)
return ;
initialized = 1 ;
@ @;
}
@ Corner orientation changes.
Frequently we need to change the orientation of a cubie. This
is easy to do for edges (just flip the low-order bit) but for
corners it is slightly more difficult. We introduce the following
arrays to allow changing corner orientations without performing
a modulo or division operation.
@=
static unsigned char corner_ori_inc[CUBIES], corner_ori_dec[CUBIES],
corner_ori_neg_strip[CUBIES], mod24[2*CUBIES] ;
@ We need to declare these in the C++ file. We also declare our
identity cube here.
@=
const cubepos identity_cube(0,0,0) ;
unsigned char cubepos::corner_ori_inc[CUBIES],
cubepos::corner_ori_dec[CUBIES],
cubepos::corner_ori_neg_strip[CUBIES], cubepos::mod24[2*CUBIES] ;
@ Initialization of these is straightforward.
@=
for (int i=0; i=
inline cubepos(const cubepos &cp=identity_cube) { *this = cp ; }
cubepos(int,int,int) ;
@ The static initialization hack.
We declare an instance of |cubepos| using this second constructor
here, so it appears in every compilation unit that uses |cubepos|, and
thus we work around the {\it static initialization fiasco} that makes
C++ so dangerous. We also declare the method that will do the work of
initializing all the static members of the class.
@=
static cubepos cubepos_initialization_hack(1,2,3) ;
@ Initializing the identity cube.
Based on the data structure definitions we have given so far, we can
write the constructor that initializes the identity position here. We
also throw in a call to the main class initialization routine, which
ensures the class is initialized for any compilation unit that
includes this header.
@(cubepos.cpp@>=
cubepos::cubepos(int,int,int) {
for (int i=0; i<8; i++)
c[i] = corner_val(i, 0) ;
for (int i=0; i<12; i++)
e[i] = edge_val(i, 0) ;
init() ;
}
@ Face numbering.
When we talk about faces of the cube, normally we refer to the color
of the face, which, on the 3x3x3 cube, is given by the center cubie,
since the center cubie cannot be displaced by normal moves. Rather
than adopt a particular color scheme, it is conventional in
mathematics about the cube to simply label each face by its spatial
location; thus, we have the Up and Down faces, the Left and Right
faces, and the Front and Back faces. These are normally abbreviated
U, F, R, D, B, and L. We number the faces so that we can use face
indices to index into arrays, and so we can number the moves which are
all twists of faces. The numbering we choose is arbitrary, but if we
choose it to have certain properties it can simplify later code. The
numbering we adopt is based on the most regular ``unfolding'' of the
cube we could find. The order we adopt is U, F, R, D, B, L, to which
we assign the ordinals 0 to 5. This ordering has the following
properties:
1. The opposite face of face $i$ is $i+3\mod |FACES|$
2. Every three cyclically consecutive faces in this ordering
join at a single corner. This defines six of the eight
corners; the other two are defined by the odd-numbered
and the even-numbered faces, respectively.
3. Every pair of faces whose ordinals do not differ by 3
(mod |FACES|) defines an edge.
@=
static char faces[FACES] ;
@ We initialize the face array here.
@=
char cubepos::faces[FACES] = {'U','F','R','D','B','L'} ;
@ Move numbering.
Once we've numbered the faces, numbering the moves is straightforward.
We order the twists in the order (clockwise, half turn,
counterclockwise) based on the idea that a clockwise twist is the
basic move, and a half turn is that move squared (or done twice) and a
counterclockwise turn is that move done three times.
We will represent a clockwise turn of the U face by the move U1 (or,
where there is no ambiguity, just U). A half-turn is represented by
U2, and a counterclockwise turn by U3. The normal convention for a
counterclockwise turn is U', but I prefer U3 for simplicity. When
discussing these moves in a mathematical context we may use $U$,
$U^2$, and $U^{-1}$, respectively.
At this point many programmers would fill the namespace with symbolic
names for each of the moves. At this point, I don't believe that
carries any benefit.
The move routine has this signature:
@=
void move(int mov) ;
@* The first move routine.
We are ready now to write our first move routine. Since we are
storing the location and orientation for each cubie, it is
straightforward to determine how a particular move will affect each
cubie. We can encode this information into a pair of small arrays,
one for corners and one for edges. Note that the impact of a move
does not depend on which corner or which edge it is, just the
current location and orientation, so we can use the same array for
all cubies.
@=
static unsigned char edge_trans[NMOVES][CUBIES],
corner_trans[NMOVES][CUBIES] ;
@ Here is the data itself.
The two arrays sum to only 864 bytes, and the values for each move are
contiguous (within each array) so this is cache-friendly.
@=
unsigned char cubepos::edge_trans[NMOVES][CUBIES],
cubepos::corner_trans[NMOVES][CUBIES] ;
@ Performing the move.
Performing the move itself is simple; we just apply the arrays above
to each cubie. We grab a pointer to the array more as a coding
shorthand than as a hint to the compiler. We manually unroll the
loop; we want to ensure the compiler gives us one short quick function
body with no branches or loops; this routine could be called many
trillions of times. Even though this code looks relatively long, it's
branch-free, cache-friendly, and appears to execute extremely fast on
modern processors such as the Intel i7-920.
@(cubepos.cpp@>=
void cubepos::move(int mov) {
const unsigned char *p = corner_trans[mov] ;
c[0] = p[c[0]] ; c[1] = p[c[1]] ; c[2] = p[c[2]] ;
c[3] = p[c[3]] ; c[4] = p[c[4]] ; c[5] = p[c[5]] ;
c[6] = p[c[6]] ; c[7] = p[c[7]] ;
p = edge_trans[mov] ;
e[0] = p[e[0]] ; e[1] = p[e[1]] ; e[2] = p[e[2]] ;
e[3] = p[e[3]] ; e[4] = p[e[4]] ; e[5] = p[e[5]] ;
e[6] = p[e[6]] ; e[7] = p[e[7]] ; e[8] = p[e[8]] ;
e[9] = p[e[9]] ; e[10] = p[e[10]] ; e[11] = p[e[11]] ;
}
@ Edge permutation.
We need to now fill in the |edge_trans| and |corner_trans| arrays.
Based on our cubie numbering, we can build an array listing the slots
that are affected by a clockwise twist of each face, in the order of
the moves, based on our slot numbering convention and move numbering
convention. A clockwise twist of the first face (U) moves the cubie
from slot 0 into slot 2, from slot 2 into slot 3, from slot 3 into
slot 1, and from slot 1 into slot 0. This is represented by the
permutation written as $(0,2,3,1)$, and this comprises the first
element of the following array. The rest are filled in similarly.
@=
static const unsigned char edge_twist_perm[FACES][4] = {
{ 0, 2, 3, 1 },
{ 3, 7, 11, 6 },
{ 2, 5, 10, 7 },
{ 9, 11, 10, 8 },
{ 0, 4, 8, 5 },
{ 1, 6, 9, 4 }
} ;
@ Corner permutation.
We can do the same thing for the corner permutation. A quarter twist
of the U face moves the corner in slot 0 to slot 1, from slot 1 to
slot 3, from slot 3 to slot 2, and from slot 2 to slot 0. This
permutation is $(0,1,3,2)$, and it's the first entry in the array
below. This array is carefully constructed so the first two slots are
always from the U face (assuming any slots are), which simplifies
some later code.
@=
static const unsigned char corner_twist_perm[FACES][4] = {
{ 0, 1, 3, 2 },
{ 2, 3, 7, 6 },
{ 3, 1, 5, 7 },
{ 4, 6, 7, 5 },
{ 1, 0, 4, 5 },
{ 0, 2, 6, 4 }
} ;
@ Edge orientation convention.
Now we consider the orientation aspects of moves. When we say a
corner is twisted, or an edge is flipped, that makes sense when the
cubie is in its solved position. But what does it mean for a cubie to
be twisted or flipped when it is in some other slot?
Let us start by considering edge flip. Consider the edge cubie whose
home location is the intersection of the U and F faces (we can call
this cubie UF). If we permit only the moves U, F, D, and B (and
half-twists and counterclockwise twists), it is straightforward to see
that whenever the cubie UF is in the U or D face, its U facelet (the
sticker colored the same as the center cubie on the U face) is always
on the U or D face, and never on one of the F, R, B, or L faces.
Further, when the UF cubie is in the middle layer, its U facelet is
always on the L or R face. In other words, there is only a single
orientation for each cubie in each slot if we start from the solved
position and perform only combinations of the moves U, F, D, and B.
If we further permit R and L moves, however, this is no longer true.
In particular, the move sequence F1R1U1 brings the UF cubie back to
the UF slot, but now the U facelet is in the front face.
We can thus define an edge orientation convention as follows. Only
the four moves R1, R3, L1, and L3 modify the edge orientation of any
cubie as the cubie moves along slots. All other moves preserve
the edge orientation.
There are a number of alternative edge orientation conventions. We
can use any pair of opposite faces instead of R and L above. Or,
interestingly, we can simply state that every quarter move flips
the edge orientation of every involved edge cubie. This last
convention has more symmetry than the one we adopt, but for reasons
that come from certain specific use of this class, we reject these
alternative orientation conventions and use the one defined here.
@=
static const unsigned char edge_change[FACES] = { 0, 0, 1, 0, 0, 1 } ;
@ Corner orientation convention.
Corner orientation is similar, but there are three possible
orientations for every cubie, not just two. Note that every cubie has
a U or D facelet; this permits a straightforward orientation
convention based on simple examination. If the U or D facelet is in
the U or D face, we declare the cubie to be properly oriented (an
orientation of 0). If twisting the cubie (when looking towards the
center of the cube from that cubie) counterclockwise brings it into
the oriented state, then we consider the cubie to be oriented
clockwise, or +1. If twisting the cubie clockwise brings it into the
oriented state, we consider the cubie to be oriented counterclockwise,
or +2 (which is $--1\mod 3$).
From this definition, it is clear that no move of the U or D faces
will change the orientation of any corner cubie. A quarter twist of
any other face that leaves a particular corner cubie in the same U or
D face that it started from will effect a clockwise twist on that
cubie. A quarter twist that moves a corner cube from the U face to
the D face, or from the D face to the U face, will effect a
counterclockwise twist on that cubie. This can be summarized in the
following array. Note that we use the information that the
|corner_twist_perm| array above always starts with two U face slots
before listing two D face slots; thus, the transition corresponding to
elements 0 and 2 preserve the U or D face of a cubie, while the
elements for 1 and 3 move a cubie from the U face to the D face or
vice versa.
@=
static const unsigned char corner_change[FACES][4] = {
{ 0, 0, 0, 0 },
{ 1, 2, 1, 2 },
{ 1, 2, 1, 2 },
{ 0, 0, 0, 0 },
{ 1, 2, 1, 2 },
{ 1, 2, 1, 2 },
} ;
@ Making the move table.
At this point we have all the data we need to fill in the
|corner_trans| and |edge_trans| arrays. This initialization routine
combines information from the four static arrays above, along with the
bit assignment for the cubie information we have defined. First we
fill in default unchanged values for all entries.
@=
for (int m=0; m=
for (int f=0; f=
typedef vector moveseq ;
@ First we declare the public methods to invert a move, move sequence,
and position. Since inverting a position will be so common, we
require that the destination be passed in by reference (which
eliminates any confusion on the part of the programmer as to when
an object will be created or destroyed).
@=
static int invert_move(int mv) { return inv_move[mv] ; }
static moveseq invert_sequence(const moveseq &sequence) ;
void invert_into(cubepos &dst) const ;
@ To invert moves, we need a quick static array.
@=
static unsigned char inv_move[NMOVES] ;
@ We add instantiate this array.
@=
unsigned char cubepos::inv_move[NMOVES] ;
@ Initialization of this array is straightforward. We simply
negate the twist component.
@=
for (int i=0; i=
moveseq cubepos::invert_sequence(const moveseq &seq) {
unsigned int len = seq.size() ;
moveseq r(len) ;
for (unsigned int i=0; i=
void cubepos::invert_into(cubepos &dst) const {
for (int i=0; i<8; i++) {
int cval = c[i] ;
dst.c[corner_perm(cval)] = corner_ori_sub(i, cval) ;
}
for (int i=0; i<12; i++) {
int cval = e[i] ;
dst.e[edge_perm(cval)] = edge_val(i, edge_ori(cval)) ;
}
}
@* The second move routine.
Now, a move sequence applied to the identity cube yields a position,
and applying the inverse sequence yields the inverse position. We can
think of a position as corresponding to an implicit move sequence that
leads to that position, and performing a move on the position extends
that sequence on the right by that move. What about extending the
sequence on the left; can we perform that operation?
Alternatively, our class as we have presented it so far contains two
arrays |c| and |e| that, for each corner and edge cubie, indicates
what slot it is in and what orientation it has in that slot. Another
representation might instead use those arrays to indicate for each
slot, what cubie is in the slot and what orientation it has.
Interestingly, these two distinct representations are exactly
inverses of each other. Using the |invert_into| method, we can
convert a position from one representation to the other.
Can we write a move routine that treats the position in the second
fashion? Yes we can, and it is fairly simple to do. We will call
this routine |movepc| because it is a move routine that treats the
representation as a slot (position) to cubie map (where our default
presentation so far has considered it as a cubie to slot map).
@=
void movepc(int mov) ;
@ One advantage of representing the cube with a slot to cubie map is,
for each move, we only need to update the specific eight slots that
are modified by a particular move. In our cubie to slot
representation, we cannot easily take such a shortcut. This advantage
is reduced by the need to branch based on the move number itself,
however; which slots are affected by a move depends on the move. It
is possible to use a table and indirection to eliminate the switch,
but half moves and quarter moves still must be distinguished in some
way, so you can't eliminate it all.
Moves on the up and down face preserve both corner and edge
orientations of the cubies, so they just move cubie values from
slot to slot. We define macros to do a swap and a four-cycle.
The first argument to these macros is the array to modify; the
remaining arguments are indices into this array.
@(cubepos.cpp@>=
#define ROT2(cc,a,b) {unsigned char t=cc[a];cc[a]=cc[b];cc[b]=t;}
#define ROT4(cc,a,b,c,d) {unsigned char t=cc[d];cc[d]=cc[c];cc[c]=cc[b]; cc[b]=cc[a];cc[a]=t;}
#define ROT22(cc,a,b,c,d) ROT2(cc,a,c) ROT2(cc,b,d)
@ Some moves change the orientation of edges. Some moves change the
orientation of corners. Looking at the definition of the
|corner_change| array, we note that the moves that change the
orientations of the corners are all the same; the even corners are
incremented and the odd ones are decremented, so we only need a single
preprocessor macro.
@(cubepos.cpp@>=
#define EDGE4FLIP(a,b,c,d) {unsigned char t=e[d];e[d]=edge_flip(e[c]);\
e[c]=edge_flip(e[b]); e[b]=edge_flip(e[a]);e[a]=edge_flip(t);}
#define CORNER4FLIP(a,b,cc,d) {unsigned char t=c[d];c[d]=corner_ori_inc[c[cc]];\
c[cc]=corner_ori_dec[c[b]];c[b]=corner_ori_inc[c[a]];c[a]=corner_ori_dec[t];}
@ With these macros, we are ready to write the routine; just a big
switch statement, then a bunch of constants indicating which slots are
affected. We do need to separate the halfturn from the quarter turn
case. All of these constants are tedious to look up so it's imperative
we test them exhaustively; luckily, the redundancy from the |move|
routine and the relationship between |move| and |movepc| makes it easy
to test. Note how the slice turn moves are just combinations of the
other moves.
@(cubepos.cpp@>=
void cubepos::movepc(int mov) {
switch(mov) {
case 0: ROT4(e,0,2,3,1); ROT4(c,0,1,3,2); break ;
case 1: ROT22(e,0,2,3,1); ROT22(c,0,1,3,2); break ;
case 2: ROT4(e,1,3,2,0); ROT4(c,2,3,1,0); break ;
case 3: ROT4(e,3,7,11,6); CORNER4FLIP(3,7,6,2); break ;
case 4: ROT22(e,3,7,11,6); ROT22(c,2,3,7,6); break ;
case 5: ROT4(e,6,11,7,3); CORNER4FLIP(3,2,6,7); break ;
case 6: EDGE4FLIP(2,5,10,7); CORNER4FLIP(1,5,7,3); break ;
case 7: ROT22(e,2,5,10,7); ROT22(c,3,1,5,7); break ;
case 8: EDGE4FLIP(7,10,5,2); CORNER4FLIP(1,3,7,5); break ;
case 9: ROT4(e,9,11,10,8); ROT4(c,4,6,7,5); break ;
case 10: ROT22(e,9,11,10,8); ROT22(c,4,6,7,5); break ;
case 11: ROT4(e,8,10,11,9); ROT4(c,5,7,6,4); break ;
case 12: ROT4(e,0,4,8,5); CORNER4FLIP(0,4,5,1); break ;
case 13: ROT22(e,0,4,8,5); ROT22(c,1,0,4,5); break ;
case 14: ROT4(e,5,8,4,0); CORNER4FLIP(0,1,5,4); break ;
case 15: EDGE4FLIP(1,6,9,4); CORNER4FLIP(2,6,4,0); break ;
case 16: ROT22(e,1,6,9,4); ROT22(c,0,2,6,4); break ;
case 17: EDGE4FLIP(4,9,6,1); CORNER4FLIP(2,0,4,6); break ;
}
}
@* The multiplication operation.
Rubik's cube is a group, and groups are defined by a multiplication
operation between arbitrary elements. So far all we have are element
definitions, move definitions, and inversion operations. It is
straightforward to write the general multiplication routine. Since
this might be performance-critical, we again require that the result
be passed in by reference, so the programmer is explicitly aware of
allocation and deallocation.
If the only operations performed for an investigation are |move|,
creation of an identity cube, equality comparison, and ordering
comparisons where the order doesn't matter (but existence of a total
order does), then |movepc| can be used instead of |move|; they simply
reflect different representations of the same group. The |movepc|
routine essentially acts with inverse generators left-multiplied by
inverse positions, rather than the normal generators right-multiplied
by positions. Essentially, we are operating on an automorphism of the
main group. So if we are using |movepc| to represent moves, we need
to use an alternative multiplication operation that reverses the
operands (to reflect that |movepc| is actually doing left
multiplication rather than right multiplication from the perspective
of our normal representation).
@=
static void mul(const cubepos &a, const cubepos &b, cubepos &r) ;
inline static void mulpc(const cubepos &a, const cubepos &b, cubepos &r) {
mul(b, a, r) ;
}
@ The multiplication routine itself is straightforward; it is
the same as a normal permutation multiplication, except we also
need to carry forward and ``add'' the orientations.
@(cubepos.cpp@>=
void cubepos::mul(const cubepos &a, const cubepos &b, cubepos &r) {
for (int i=0; i<8; i++) {
int cc = a.c[i] ;
r.c[i] = corner_ori_add(b.c[corner_perm(cc)], cc) ;
}
for (int i=0; i<12; i++) {
int cc = a.e[i] ;
r.e[i] = edge_ori_add(b.e[edge_perm(cc)], cc) ;
}
}
@* Parsing and printing moves and move sequences.
Cube programs frequently require input and output of moves and move
sequences. This section provides some simple routines to support
this.
@=
static void skip_whitespace(const char *&p) ;
static int parse_face(const char *&p) ;
static int parse_face(char f) ;
static void append_face(char *&p, int f) { *p++ = faces[f] ; }
static int parse_move(const char *&p) ;
static void append_move(char *&p, int mv) ;
static moveseq parse_moveseq(const char *&p) ;
static void append_moveseq(char *&p, const moveseq &seq) ;
static char *moveseq_string(const moveseq &seq) ;
@ We start with a buffer that's usually big enough to hold a result.
Note that use of this static buffer means the |_string()| methods
are not thread-safe.
@=
static char static_buf[200] ;
@ The routines themselves are straightforward.
@(cubepos.cpp@>=
void cubepos::skip_whitespace(const char *&p) {
while (*p && *p <= ' ')
p++ ;
}
int cubepos::parse_face(const char *&p) {
int f = parse_face(*p) ;
if (f >= 0)
p++ ;
return f ;
}
int cubepos::parse_face(char f) {
switch (f) {
case 'u': case 'U': return 0 ;
case 'f': case 'F': return 1 ;
case 'r': case 'R': return 2 ;
case 'd': case 'D': return 3 ;
case 'b': case 'B': return 4 ;
case 'l': case 'L': return 5 ;
default:
return -1 ;
}
}
int cubepos::parse_move(const char *&p) {
skip_whitespace(p) ;
const char *q = p ;
int f = parse_face(q) ;
if (f < 0)
return -1 ;
int t = 0 ;
switch (*q) {
case '1': case '+': t = 0 ; break ;
case '2': t = 1 ; break ;
case '3': case '\'': case '-': t = TWISTS-1 ; break ;
default:
return -1 ;
}
p = q + 1 ;
return f * TWISTS + t ;
}
@ And they keep going on.
@(cubepos.cpp@>=
void cubepos::append_move(char *&p, int mv) {
append_face(p, mv/TWISTS) ;
*p++ = "123"[mv % TWISTS] ;
*p = 0 ;
}
moveseq cubepos::parse_moveseq(const char *&p) {
moveseq r ;
int mv ;
while ((mv=parse_move(p)) >= 0)
r.push_back(mv) ;
return r ;
}
void cubepos::append_moveseq(char *&p, const moveseq &seq) {
*p = 0 ;
for (unsigned int i=0; i 65)
error("! can't print a move sequence that long") ;
char *p = static_buf ;
append_moveseq(p, seq) ;
return static_buf ;
}
@* Singmaster notation.
The standard format for cube positions in called Singmaster positional
notation after David Singmaster's early work on cube math. His
notation represents a solved cube in cubie order, giving the cubie
in each slot along with the orientation; he orders the slots in a
particular way, and lists a default orientation for each cubie.
In his notation, a solved cube is represented by the string
@=
static const char *sing_solved =
"UF UR UB UL DF DR DB DL FR FL BR BL UFR URB UBL ULF DRF DFL DLB DBR" ;
@ The edge cubies are listed first, followed by the corner cubies. Note
that his representation does not have any implicit orientation
convention; it is a direct permutation mapping of all 48 cubie
stickers on the edge and corner cubies. His ordering differs from
ours, but we can give our cubie numbering (complete with orientation)
in a pair of literal arrays. For the corners, we actually list
48 values; the other 24 will come into play later, when we consider
symmetry.
@=
static const char *const smedges[] = {
"UB", "BU", "UL", "LU", "UR", "RU", "UF", "FU",
"LB", "BL", "RB", "BR", "LF", "FL", "RF", "FR",
"DB", "BD", "DL", "LD", "DR", "RD", "DF", "FD",
} ;
static const char *const smcorners[] = {
"UBL", "URB", "ULF", "UFR", "DLB", "DBR", "DFL", "DRF",
"LUB", "BUR", "FUL", "RUF", "BDL", "RDB", "LDF", "FDR",
"BLU", "RBU", "LFU", "FRU", "LBD", "BRD", "FLD", "RFD",
"ULB", "UBR", "UFL", "URF", "DBL", "DRB", "DLF", "DFR",
"LBU", "BRU", "FLU", "RFU", "BLD", "RBD", "LFD", "FRD",
"BUL", "RUB", "LUF", "FUR", "LDB", "BDR", "FDL", "RDF",
} ;
@ We can parse the Singmaster notation by parsing cubies. One good way
to parse cubies is to turn each cubie into a base-6 number with a
leading 1; the leading 1 gives us information on how many face
specifications we saw. We also need information on the order of
the cubies in the Singmaster notation. The following arrays collect
some information useful in this parsing. We use the value
99 as a marker of an invalid cubie.
@=
const int INVALID = 99 ;
static unsigned char lookup_edge_cubie[FACES*FACES] ;
static unsigned char lookup_corner_cubie[FACES*FACES*FACES] ;
static unsigned char sm_corner_order[8] ;
static unsigned char sm_edge_order[12] ;
static unsigned char sm_edge_flipped[12] ;
@ The following routines parse a generic cubie, returning a base-6
number with a leading 1, and then routines that parse edges and
cubies, respectively.
@=
static int parse_cubie(const char *&p) {
cubepos::skip_whitespace(p) ;
int v = 1 ;
int f = 0 ;
while ((f=cubepos::parse_face(p)) >= 0) {
v = v * 6 + f ;
if (v >= 2 * 6 * 6 * 6)
return -1 ;
}
return v ;
}
static int parse_edge(const char *&p) {
int c = parse_cubie(p) ;
if (c < 6 * 6 || c >= 2 * 6 * 6)
return -1 ;
c = lookup_edge_cubie[c-6*6] ;
if (c == INVALID)
return -1 ;
return c ;
}
static int parse_corner(const char *&p) {
int c = parse_cubie(p) ;
if (c < 6 * 6 * 6 || c >= 2 * 6 * 6 * 6)
return -1 ;
c = lookup_corner_cubie[c-6*6*6] ;
if (c == INVALID || c >= CUBIES)
return -1 ;
return c ;
}
@ We need to initialize all of those arrays.
@=
memset(lookup_edge_cubie, INVALID, sizeof(lookup_edge_cubie)) ;
memset(lookup_corner_cubie, INVALID, sizeof(lookup_corner_cubie)) ;
for (int i=0; i=
const char *parse_Singmaster(const char *p) ;
char *Singmaster_string() const ;
@ To parse the Singmaster notation, we simply parse the input, reading
each cubie, and store it in the appropriate slot after doing any
orientation correction that might be needed. If it leads with {\tt
SING} we permit this and skip it. Note that our internal
representation is cubie to position, and Singmaster notation shows
position to cubie, so we invert as we read.
@=
const char *cubepos::parse_Singmaster(const char *p) {
if (strncmp(p, "SING ", 5) == 0)
p += 5 ;
int m = 0 ;
for (int i=0; i<12; i++) {
int c = parse_edge(p) ^ sm_edge_flipped[i] ;
if (c < 0)
return "No such edge" ;
e[edge_perm(c)] = edge_val(sm_edge_order[i], edge_ori(c)) ;
m |= 1<*=
char *cubepos::Singmaster_string() const {
cubepos cp ;
invert_into(cp) ;
char *p = static_buf ;
for (int i=0; i<12; i++) {
if (i != 0)
*p++ = ' ' ;
int j = sm_edge_order[i] ;
const char *q = smedges[cp.e[j] ^ sm_edge_flipped[i]] ;
*p++ = *q++ ;
*p++ = *q++ ;
}
for (int i=0; i<8; i++) {
*p++ = ' ' ;
int j = sm_corner_order[i] ;
const char *q = smcorners[cp.c[j]] ;
*p++ = *q++ ;
*p++ = *q++ ;
*p++ = *q++ ;
}
*p = 0 ;
return static_buf ;
}
@* Symmetry.
Our next consideration is whole cube rotations and the symmetries of
the cube. Our labeling of the cube uses the names up, down, left,
right, back, and front intentionally---we care only about what face is
up, not what color it is. Since the color of a face is defined by the
color of the center cubie, whole cube rotations change the mappings of
colors to spatial orientations.
Since our representation does not denote the actual colors that are on
each face, we cannot represent cube rotations directly. We only
represent how the cubies move. But we {\it can} represent
conjugations of whole cube rotations---that is, given a cube rotation
$m\in M$, we can compute the operation $m p m'$ for any cube position
$p$, and this suffices for us to take advantage of the symmetries of
the cube. In this expression, the $m$ operation rotates the whole
cube and thus moves the face centers; $p$ performs operations on all
cubies except the face centers, and then $m'$ rotates a cube again,
moving the face centers back to where they originally were.
The whole cube rotations $M$ are themselves a group, but a slightly
larger one than may first appear. If you consider a physical cube and
its rotations, you can choose any specific color to be the up face,
and once that is done, you have four possibilities for the front face.
Thus, we see a physical cube has 24 possible rotations. We only
consider rotations that map faces onto faces, not partial rotations.
If we examine the cube in a mirror, we see an additional 24 ``mirror
image'' rotations that are possible---these for instance might
exchange front for back, but leave left, right, up, and down the same.
The cube has three orthogonal axes---one passing through the up and
down faces (which we call UD), one passing through the left and right
faces (LR), and one passing through the front and back faces (FB). If
we consider the U, F, and R faces to be the positive side of each of
these axes, then we can decompose the rotation group $M$ into a map of
axes onto themselves, and an indication of which of the axes are
inverted.
We assign ordinal numbers to the elements of $M$ in a useful way. We
assign the ordinal 0 to the identity element of $M$. We collect the
elements of $m$ that share the same axis mappings into contiguous
groups of eight; we have six such groups.
To manage remappings, we have an array that represents the face
remappings for each $m\in M$, another that represents the
multiplication operation for $M$, another that represents the inverse
operation in $M$, and another that represents the move mapping for
$M$. Finally, we need mapping arrays for the corners and edges for
all the different remappings.
@=
static unsigned char face_map[M][FACES], move_map[M][NMOVES] ;
static unsigned char invm[M], mm[M][M] ;
static unsigned char rot_edge[M][CUBIES], rot_corner[M][CUBIES] ;
@ These arrays must be instantiated.
@=
unsigned char cubepos::face_map[M][FACES], cubepos::move_map[M][NMOVES] ;
unsigned char cubepos::mm[M][M], cubepos::invm[M] ;
unsigned char cubepos::rot_edge[M][CUBIES], cubepos::rot_corner[M][CUBIES] ;
@ Everything comes from facemap. We cluster the elements of |M|
in groups of eight such that each group has the same axis mapping.
The axis mappings are defined by the following six cubies.
Note that the first two maintain the up/down axei, and they come
in pairs that share the same up/down axis. Within each group,
we negate different subsets of the axes. We order these carefully,
so the even ones are the ``normal'' cube rotations, and the odd ones
are the ``mirrored'' cube rotations. Indeed, the |axis_negate_map|
uses a gray-code-style ordering.
@=
static const char *const axis_permute_map[] =
{"UFR","URF","FRU","FUR","RUF","RFU"} ;
static const char *const axis_negate_map[] =
{"UFR","UFL","UBL","UBR","DBR","DBL","DFL","DFR"} ;
@ Given the three faces in the UFR corner, we can easily fill out a
face map entry. We just parse the corner one face at a time, inserting
those values in the face map entry and the opposite face offset by three.
@=
static void parse_corner_to_facemap(const char *p, unsigned char *a) {
for (int i=0; i<3; i++) {
int f = cubepos::parse_face(p[i]) ;
a[i] = f ;
a[i+3] = (f + 3) % FACES ;
}
}
@ We also provide a routine that does permutation multiplication
for a facemap.
@=
static void face_map_multiply(unsigned char *a, unsigned char *b,
unsigned char *c) {
for (int i=0; i<6; i++)
c[i] = b[a[i]] ;
}
@ With this code, generating the face map is easy; we simply generate
the basic axis permutation elements, then the axis negation elements,
and finally we do multiplication to fill out the table. The move
map is also easy to compute, since we ordered the elements in such
a way that determining which elements are negative (or mirror)
mappings is straightforward.
@=
unsigned char face_to_m[FACES*FACES*FACES] ;
for (int i=0; i<6; i++)
parse_corner_to_facemap(axis_permute_map[i], face_map[8*i]) ;
for (int i=0; i<8; i++)
parse_corner_to_facemap(axis_negate_map[i], face_map[i]) ;
for (int i=1; i<6; i++)
for (int j=1; j<8; j++)
face_map_multiply(face_map[8*i], face_map[j], face_map[8*i+j]) ;
@ Now we calculate the multiplication table and the inverse table
for $M$. We also calculate the move map.
@=
for (int i=0; i> 3)) & 1 ;
for (int f=0; f<6; f++) {
for (int t=0; t=
for (int m=0; m=
void remap_into(int m, cubepos &dst) const ;
void canon_into48(cubepos &dst) const ;
void canon_into48_aux(cubepos &dst) const ;
void canon_into96(cubepos &dst) const ;
@ We declare a pair of constants for move masks.
@=
const int ALLMOVEMASK = (1<=
void cubepos::remap_into(int m, cubepos &dst) const {
int mprime = invm[m] ;
for (int i=0; i<8; i++) {
int c1 = rot_corner[mprime][i] ;
int c2 = corner_ori_add(c[corner_perm(c1)], c1) ;
dst.c[i] = rot_corner[m][c2] ;
}
for (int i=0; i<12; i++) {
int c1 = rot_edge[mprime][i*2] ;
int c2 = edge_ori_add(e[edge_perm(c1)], c1) ;
dst.e[i] = rot_edge[m][c2] ;
}
}
@ To canonicalize 48 ways, we could just call the above routine 48
times, and choose the least. But we can eliminate most of the
possibilities more quickly with a slightly more complex routine.
@(cubepos.cpp@>=
void cubepos::canon_into48_aux(cubepos &dst) const {
for (int m=1; m dst.c[i])
goto nextm ;
}
for (int i=0; i<12; i++) {
int c1 = rot_edge[mprime][i*2] ;
int c2 = edge_ori_add(e[edge_perm(c1)], c1) ;
int t = rot_edge[m][c2] ;
if (isless || t < dst.e[i]) {
dst.e[i] = t ;
isless = 1 ;
} else if (t > dst.e[i])
goto nextm ;
}
nextm: ;
}
}
void cubepos::canon_into48(cubepos &dst) const {
dst = *this ;
canon_into48_aux(dst) ;
}
@ Frequently we need a random cube position. This should be
a truly random position, rather than one generated by a
finite number of random moves. We make this a destructive
|randomize()| call on the current cubepos.
@=
void randomize() ;
@ To implement this, we shuffle the cubies around, and then
update all the orientations randomly. When we shuffle the
cubies it's critical we maintain parity. So we only do
random selection for the first ten edges; parity forces
the eleventh edge, and of course the twelfth is forced because
it is the only one left.
@(cubepos.cpp@>=
void cubepos::randomize() {
int parity = 0 ;
for (int i=0; i<7; i++) {
int j = i + (int)((8-i)*myrand()) ;
if (i != j) {
swap(c[i], c[j]) ;
parity++ ;
}
}
for (int i=0; i<11; i++) {
int j = i + (int)((12-i)*myrand()) ;
if (i != j) {
swap(e[i], e[j]) ;
parity++ ;
}
}
if (parity & 1)
swap(e[10], e[11]) ;
int s = 24 ;
for (int i=0; i<7; i++) {
int a = (int)(3*myrand()) ;
s -= a ;
c[i] = corner_val(corner_perm(c[i]), a) ;
}
c[7] = corner_val(corner_perm(c[7]), s % 3) ;
s = 0 ;
for (int i=0; i<11; i++) {
int a = (int)(2*myrand()) ;
e[i] = edge_ori_add(e[i], a) ;
s ^= a ;
}
e[11] ^= s ;
}
@ The canonicalization into 96 is the same, but it selects the
lesser of the two to start with, and then canonicalizes into the
destination one after the other.
@(cubepos.cpp@>=
void cubepos::canon_into96(cubepos &dst) const {
cubepos cpi ;
invert_into(cpi) ;
if (*this < cpi) {
dst = *this ;
} else {
dst = cpi ;
}
canon_into48_aux(dst) ;
cpi.canon_into48_aux(dst) ;
}
@ When exploring the state space recursively, it is import for
efficiency to prune the search as early as possible. One way to do
this is to be efficient in the move sequences we explore. For
instance, it is never advantageous to consider a sequence that has two
consecutive turns of the same face, in the half turn metric, because
such a position can always be obtained from a shorter sequence.
Furthermore, consecutive rotations of opposite faces should always
occur in the same order (we choose earlier numbered faces first).
@=
const int CANONSEQSTATES = FACES+1 ;
const int CANONSEQSTART = 0 ;
@ We need a couple of small arrays to give us the next state and the
bit mask of allowed moves.
@=
static unsigned char canon_seq[CANONSEQSTATES][NMOVES] ;
static int canon_seq_mask[CANONSEQSTATES] ;
static int canon_seq_mask_ext[CANONSEQSTATES] ;
@ We instantiate these arrays.
@=
unsigned char cubepos::canon_seq[CANONSEQSTATES][NMOVES] ;
int cubepos::canon_seq_mask[CANONSEQSTATES] ;
int cubepos::canon_seq_mask_ext[CANONSEQSTATES] ;
@ Initializing these arrays is pretty easy based on the rules we have
outlined. In the halfturn metric, the state is just one plus the
previous face that was twisted.
@=
for (int s=0; s=
static inline int next_cs(int cs, int mv) { return canon_seq[cs][mv] ; }
static inline int cs_mask(int cs) { return canon_seq_mask[cs] ; }
static inline int cs_mask_ext(int cs) { return canon_seq_mask_ext[cs] ; }
@ We finish with a number of generic utility routines for cube
programming, such as error reporting, calculating the duration between
two points, and random number generation.
@=
void error(const char *s) ;
inline double myrand() { return drand48() ; }
inline int random_move() { return (int)(NMOVES*myrand()) ; }
inline int random_move_ext() { return (int)(NMOVES*myrand()) ; }
double walltime() ;
double duration() ;
@ Implementing these methods is straightforward.
@(cubepos.cpp@>=
void error(const char *s) {
cerr << s << endl ;
if (*s == '!')
exit(10) ;
}
static double start ;
double walltime() {
struct timeval tv ;
gettimeofday(&tv, 0) ;
return tv.tv_sec + 0.000001 * tv.tv_usec ;
}
double duration() {
double now = walltime() ;
double r = now - start ;
start = now ;
return r ;
}
@* Testing.
This class is no good to us if it doesn't work, and what better way to
verify it works than in isolation. This program tests cubepos, and
also provides an example of how to use it.
@(cubepos_test.cpp@>=
#include
#include *