I\'ve been practicing for an upcoming programming competition and I have stumbled across a question that I am just completely bewildered at. However, I feel as though it\'s
Here's a correct O(1) solution, but for the case where the knight moves like a chess knight only, and on an infinite chess board:
https://jsfiddle.net/graemian/5qgvr1ba/11/
The key to finding this is to notice the patterns that emerge when you draw the board. In the diagram below, the number in the square is the minimum number of moves needed to reach that square (you can use breadth-first search to find this):
Because the solution is symmetrical across the axes and the diagonals, I've only drawn the x >= 0 and y >= x case.
The bottom-left block is the starting position and the numbers in the blocks represent the minimum number of moves to reach those blocks.
There are 3 patterns to notice:
(Make sure you see both sets of diagonals as top-left to bottom-right. They have a constant move-count. The bottom-left top-right diagonals are much more complex.)
You can derive formulas for each. The yellow blocks are special cases. So the solution becomes:
function getMoveCountO1(x, y) {
var newXY = simplifyBySymmetry(x, y);
x = newXY.x;
y = newXY.y;
var specialMoveCount = getSpecialCaseMoveCount(x ,y);
if (specialMoveCount !== undefined)
return specialMoveCount;
else if (isVerticalCase(x, y))
return getVerticalCaseMoveCount(x ,y);
else if (isPrimaryDiagonalCase(x, y))
return getPrimaryDiagonalCaseMoveCount(x ,y);
else if (isSecondaryDiagonalCase(x, y))
return getSecondaryDiagonalCaseMoveCount(x ,y);
}
with the hardest being the vertical groups:
function isVerticalCase(x, y) {
return y >= 2 * x;
}
function getVerticalCaseMoveCount(x, y) {
var normalizedHeight = getNormalizedHeightForVerticalGroupCase(x, y);
var groupIndex = Math.floor( normalizedHeight / 4);
var groupStartMoveCount = groupIndex * 2 + x;
return groupStartMoveCount + getIndexInVerticalGroup(x, y);
}
function getIndexInVerticalGroup(x, y) {
return getNormalizedHeightForVerticalGroupCase(x, y) % 4;
}
function getYOffsetForVerticalGroupCase(x) {
return x * 2;
}
function getNormalizedHeightForVerticalGroupCase(x, y) {
return y - getYOffsetForVerticalGroupCase(x);
}
See the fiddle for the other cases.
Maybe there are simpler or more elegant patterns I missed? If so, I would love to see them. In particular, I notice some diagonal patterns in the blue vertical cases, but I haven't explored them. Regardless, this solution still satisfies the O(1) constraint.
Here is a solution for this particular problem implemented in Perl. It will show one of the shortest paths - there might be more than one in some cases.
I didn't use any of the algorithms described above - but it would be nice to compare it to other solutions.
#!/usr/local/bin/perl -w
use strict;
my $from = [0,0];
my $to = [7,7];
my $f_from = flat($from);
my $f_to = flat($to);
my $max_x = 7;
my $max_y = 7;
my @moves = ([-1,2],[1,2],[2,1],[2,-1],[1,-2],[-1,-2],[-2,-1],[-2,1]);
my %squares = ();
my $i = 0;
my $min = -1;
my @s = ( $from );
while ( @s ) {
my @n = ();
$i++;
foreach my $s ( @s ) {
unless ( $squares{ flat($s) } ) {
my @m = moves( $s );
push @n, @m;
$squares{ flat($s) } = { i=>$i, n=>{ map {flat($_)=>1} @m }, };
$min = $i if $squares{ flat($s) }->{n}->{$f_to};
}
}
last if $min > -1;
@s = @n;
}
show_path( $f_to, $min );
sub show_path {
my ($s,$i) = @_;
return if $s eq $f_from;
print "$i => $f_to\n" if $i == $min;
foreach my $k ( keys %squares ) {
if ( $squares{$k}->{i} == $i && $squares{$k}->{n}->{$s} ) {
$i--;
print "$i => $k\n";
show_path( $k, $i );
last;
}
}
}
sub flat { "$_[0]->[0],$_[0]->[1]" }
sub moves {
my $c = shift;
my @s = ();
foreach my $m ( @moves ) {
my $x = $c->[0] + $m->[0];
my $y = $c->[1] + $m->[1];
if ( $x >= 0 && $x <=$max_x && $y >=0 && $y <=$max_y) {
push @s, [$x, $y];
}
}
return @s;
}
__END__
I first encountered this problem in a Codility test. They gave me 30 minutes to solve it - it took me considerably longer than that to get to this result! The problem was: how many moves does it take for a knight to go from 0,0 to x,y using only legal Knight's moves. x and y were more-or-less unbounded (so we're not talking here about a simple 8x8 chessboard).
They wanted an O(1) solution. I wanted a solution where the program was clearly solving the problem (i.e. I wanted something more obviously right than Graeme's pattern - patterns have a habit of breaking down where you're not looking), and I really wanted not to have to rely on an unargued formula, as in Mustafa's solution
So, here's my solution, for what it's worth. Start, as others have, by noting the solution is symmetrical about the axes and diagonals, so we need to solve only for 0 >= y >= x. For simplicity of explanation (and code) I'm going to reverse the problem: the knight starts at x,y, and is aiming for 0,0.
Let's suppose we shrink the problem down to the vicinity of the origin. We'll get to what 'vicinty' actually means in due course, but for now, let's just write some solutions down in a cheatsheet (origin at bottom left):
2 1 4 3
3 2 1 2
0 3 2 3
So, given x,y on the grid, we can just read off the number of moves to the origin.
If we've started outside the grid, we have to work our way back to it. We introduce the 'midline', which is the line represented by y=x/2. Any knight at x,y on that line can work its way back to the cheatsheet using a series of 8 o'clock moves (that is: (-2,-1) moves). If x,y lies above the midline, then we'll need a succession of 8 o'clock and 7 o'clock moves, and if it lies below the midline, we'll need a succession of 8 o'clock and 10 o'clock moves. Two things to note here:
So, let's look at the above-midline moves. What we are claiming is that:
(dx;dy) = (2,1 ; 1,2) (n8; n7) (matrix notation, without math typesetting - column vector (dx;dy) equals the square matrix multiplied by column vector (n8;n7) - the number of 8 o'clock moves and the number of 7 o'clock moves), and similarly;
(dx;dy) = (2,2; 1,-1) (n8; n10)
I'm claiming that dx,dy will be roughly (x,y), so (x-dx, y-dy) will be in the vicinity of the origin (whatever 'vicinity' turns out to be).
The two lines in the code which compute these terms are the solution to these, but they're selected to have some useful properties:
(Would you like proofs of these?) So, the Knight's distance will be the sum of n7, n8, n10 and cheatsheet [x-dx, y-dy], and our cheatsheet reduces to this:
. . 4
. 2 .
0 3 2
Now, this isn't quite the end of the story. Look at the 3 on the bottom row. The only ways we can reach this are either:
There's a similar optimisation to be had with the 4 at top right. Apart from starting there, the only way to reach that is by an 8 o'clock move from (4,3). That's not on the cheatsheet, but if it were there, its distance would be 3, because we could have 7 o'clocked to (3,1) instead, which has a distance of only 2. So, we should back-track one 8-o'clock move, and then go forward one 7-o'clock.
So, we need to add one more number to the cheatsheet:
. . 4
. 2 . 2
0 3 2
(Note: there are a whole load of back-tracking optimisations from (0,1) and (0,2) but since the solver will never take us there, we don't need to worry about them.)
So here, then, is some Python code to evaluate this:
def knightDistance (x, y):
# normalise the coordinates
x, y = abs(x), abs(y)
if (x<y): x, y = y, x
# now 0 <= y <= x
# n8 means (-2,-1) (8 o'clock), n7 means (-1,-2) (7 o'clock), n10 means (-2,+1) (10 o'clock)
if (x>2*y):
# we're below the midline. Using 8- & 10-o'clock moves
n7, n8, n10 = 0, (x + 2*y)//4, (x - 2*y + 1)//4
else:
# we're above the midline. Using 7- and 8-o'clock moves
n7, n8, n10 = (2*y - x)//3, (2*x - y)//3, 0
x -= 2*n8 + n7 + 2*n10
y -= n8 + 2*n7 - n10
# now 0<=x<=2, and y <= x. Also (x,y) != (2,1)
# Try to optimise the paths.
if (x, y)==(1, 0): # hit the 3. Did we need to?
if (n8>0): # could have passed through the 2 at 3,1. Back-up
x, y = 3, 1; n8-=1;
if (x, y)==(2, 2): # hit the 4. Did we need to?
if (n8>0): # could have passed through a 3 at 4,3. Back-up, and take 7 o'clock to 2 at 3,1
x, y = 3, 1; n8-=1; n7+=1
# Almost there. Now look up the final leg
cheatsheet = [[0, 3, 2], [2, None, 2], [4]]
return n7 + n8 + n10 + cheatsheet [y][x-y]
Incidentally, if you want to know an actual route, then this algorithm provides that too: it is simply a succession of n7 7-o'clock moves, followed by (or interspersed with) n8 8-o'clock moves, n10 10-o'clock moves, and whatever dance is dictated by the cheatsheet (which, itself, can be in a cheatsheet).
Now: How to prove this is right. It's not enough just to compare these results with a table of right answers, because the problem itself is unbounded. But we can say that, if the Knight's distance of a square s is d, then if {m} is the set of legal moves from s, the Knight's distance of (s+m) must be either d-1 or d+1 for all m. (Do you need a proof of this?) Furthermore, there must be at least one such square whose distance is d-1, unless s is the origin. So, we can prove correctness by showing this property holds for every square. Thus:
def validate (n):
def isSquareReasonable (x, y):
d, downhills = knightDistance (x, y), 0
moves = [(1, 2), (2, 1), (2, -1), (1, -2), (-1, -2), (-2, -1), (-2, 1), (-1, 2)]
for dx, dy in moves:
dd = knightDistance (x+dx, y+dy)
if (dd == d+1): pass
elif (dd== d-1): downhills += 1
else: return False;
return (downhills>0) or (d==0)
for x in range (0, n+1):
for y in range (0, n+1):
if not isSquareReasonable (x, y): raise RuntimeError ("Validation failed")
Alternatively, we can prove the correctness of any one square s by chasing the route from s downhill to the origin. First, check s for reasonableness as above, then select any s+m such that distance (s+m) == d-1. Repeat until we reach the origin.
Howzat?
Yes, Dijkstra and BFS will get you the answer, but I think the chess context of this problem provides knowledge that can yield a solution that is much faster than a generic shortest-path algorithm, especially on an infinite chess board.
For simplicity, let's describe the chess board as the (x,y) plane. The goal is to find the shortest path from (x0,y0) to (x1,y1) using only the candidate steps (+-1, +-2), (+-2, +-1), and (+-2, +-2), as described in the question's P.S.
Here is the new observation: draw a square with corners (x-4,y-4), (x-4,y+4), (x+4,y-4), (x+4,y+4). This set (call it S4) contains 32 points. The shortest path from any of these 32 points to (x,y) requires exactly two moves.
The shortest path from any of the 24 points in the set S3 (defined similarly) to (x,y) requires at least two moves.
Therefore, if |x1-x0|>4 or |y1-y0|>4, the shortest path from (x0,y0) to (x1,y1) is exactly two moves greater than the shortest path from (x0,y0) to S4. And the latter problem can be solved quickly with straightforward iteration.
Let N = max(|x1-x0|,|y1-y0|). If N>=4, then the shortest path from (x0,y0) to (x1,y1) has ceil(N/2) steps.
The O(1) answer above [https://stackoverflow.com/a/8778592/4288232 by Mustafa Serdar Şanlı] isn't really working. (Check (1,1) or (3,2) or (4,4), aside for the obvious edge cases of (1,0) or (2,2)).
Below is a much uglier solution (python), which does work (with added "tests"):
def solve(x,y):
x = abs(x)
y = abs(y)
if y > x:
temp=y
y=x
x=temp
if (x==2 and y==2):
return 4
if (x==1 and y==0):
return 3
if(y == 0 or float(y) / float(x) <= 0.5):
xClass = x % 4
if (xClass == 0):
initX = x/2
elif(xClass == 1):
initX = 1 + (x/2)
elif(xClass == 2):
initX = 1 + (x/2)
else:
initX = 1 + ((x+1)/2)
if (xClass > 1):
return initX - (y%2)
else:
return initX + (y%2)
else:
diagonal = x - ((x-y)/2)
if((x-y)%2 == 0):
if (diagonal % 3 == 0):
return (diagonal/3)*2
if (diagonal % 3 == 1):
return ((diagonal/3)*2)+2
else:
return ((diagonal/3)*2)+2
else:
return ((diagonal/3)*2)+1
def test():
real=[
[0,3,2,3,2,3,4,5,4,5,6,7,6,7],
[3,2,1,2,3,4,3,4,5,6,5,6,7,8],
[2,1,4,3,2,3,4,5,4,5,6,7,6,7],
[3,2,3,2,3,4,3,4,5,6,5,6,7,8],
[2,3,2,3,4,3,4,5,4,5,6,7,6,7],
[3,4,3,4,3,4,5,4,5,6,5,6,7,8],
[4,3,4,3,4,5,4,5,6,5,6,7,6,7],
[5,4,5,4,5,4,5,6,5,6,7,6,7,8],
[4,5,4,5,4,5,6,5,6,7,6,7,8,7],
[5,6,5,6,5,6,5,6,7,6,7,8,7,8],
[6,5,6,5,6,5,6,7,6,7,8,7,8,9],
[7,6,7,6,7,6,7,6,7,8,7,8,9,8]]
for x in range(12):
for y in range(12):
res = solve(x,y)
if res!= real[x][y]:
print (x, y), "failed, and returned", res, "rather than", real[x][y]
else:
print (x, y), "worked. Cool!"
test()
What you need to do is think of the possible moves of the knight as a graph, where every position on the board is a node and the possible moves to other position as an edge. There is no need for dijkstra's algorithm, because every edge has the same weight or distance (they are all just as easy or short to do). You can just do a BFS search from your starting point until you reach the end position.