3

There are tones of solutions for Knights tour or shortest path for Knights movement from source cell to destination cell. most of the solutions are using BFS which seems the best algorithm.

Here is my implementation using HashMap:

    public class Knight_HashMap {

    static HashMap<String, Position> chessboard = new HashMap<String, Position>();
    static Queue<Position> q = new LinkedList<Position>();
    static int Nx, Ny, Kx, Ky, Cx, Cy;

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        System.out.println("insert Board dimentions: Nx, Ny");
        Nx = sc.nextInt();
        Ny = sc.nextInt();
        System.out.println("inset Knight's location: Kx, Ky");
        Kx = sc.nextInt();
        Ky = sc.nextInt();
        System.out.println("insert destination location: Cx, Cy");
        Cx = sc.nextInt();
        Cy = sc.nextInt();
        sc.close();

        // Assume the position for simplicity. In real world, accept the values using
        // Scanner.
        Position start = new Position(Kx, Ky, 0); // Positionition 0, 1 on the chessboard
        Position end = new Position(Cx, Cy, Integer.MAX_VALUE);

        chessboard.put(Arrays.toString(new int[] { Kx, Ky }), new Position(Kx, Ky, 0));

        q.add(start);

        while (q.size() != 0) // While queue is not empty
        {
            Position pos = q.poll();
            if (end.equals(pos)) {
                System.out.println("Minimum jumps required: " + pos.depth);
                return;
            } else {
                // perform BFS on this Position if it is not already visited
                bfs(pos, ++pos.depth);
            }
        }

    }

    private static void bfs(Position current, int depth) {

        // Start from -2 to +2 range and start marking each location on the board
        for (int i = -2; i <= 2; i++) {
            for (int j = -2; j <= 2; j++) {

                Position next = new Position(current.x + i, current.y + j, depth);

                if (isValid(current, next)) {
                    if (inRange(next.x, next.y)) {
                        // chessboard.put(Arrays.toString(new int[] { next.x, next.y }), next);
                        // Skip if next location is same as the location you came from in previous run
                        if (current.equals(next))
                            continue;

                        Position position = chessboard.get(Arrays.toString(new int[] { next.x, next.y }));
                        if (position == null) {
                            position = new Position(Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE);
                        }
                        /*
                         * Get the current position object at this location on chessboard. If this
                         * location was reachable with a costlier depth, this iteration has given a
                         * shorter way to reach
                         */
                        if (position.depth > depth) {

                            chessboard.put(Arrays.toString(new int[] { current.x + i, current.y + j }),
                                    new Position(current.x, current.y, depth));
                            // chessboard.get(current.x + i).set(current.y + j, new Position(current.x,
                            // current.y, depth));
                            q.add(next);
                        }
                    }
                }

            }

        }

    }

    private static boolean isValid(Position current, Position next) {
        // Use Pythagoras theorem to ensure that a move makes a right-angled triangle
        // with sides of 1 and 2. 1-squared + 2-squared is 5.
        int deltaR = next.x - current.x;
        int deltaC = next.y - current.y;
        return 5 == deltaR * deltaR + deltaC * deltaC;
    }

    private static boolean inRange(int x, int y) {
        return 0 <= x && x < Nx && 0 <= y && y < Ny;
    }

}

class Position {

    public int x;
    public int y;
    public int depth;

    Position(int x, int y, int depth) {
        this.x = x;
        this.y = y;
        this.depth = depth;
    }

    public boolean equals(Position that) {
        return this.x == that.x && this.y == that.y;
    }

    public String toString() {
        return "(" + this.x + " " + this.y + " " + this.depth + ")";
    }
}

This works well with small dimension but with 10^9 x 10^9 I face outOfMemory exception. I also tried with java 2d array, ArrayList, HashMap alongside with a LinkedList as Queue. but for that dimension 10^9 x 10^9 with any data structure, I face outOfMemory exception.

Is there possibility of optimization to avoid outOfMemory or anyother way/data structure to handle such huge dimension?

Note: I should mention this question is from BAPC17 contest named Knight's Marathon

xskxzr
  • 7,613
  • 5
  • 24
  • 47
Amir-Mousavi
  • 268
  • 2
  • 13

5 Answers5

5

There is a closed form solution for finding the minimum number of moves the chess knight needs to move a specified displacement on the infinite chess board. Let $g$ be the requisite displacement expressed as a Gaussian integer; the real part of $g$ will be the horizontal displacement, and the imaginary part of $g$ will be the vertical displacement. Then we may write

$$g = ((1-i)g+(2-i)d)(2+i) - (g+(2+i)d)(2-i). $$

Here, $d$ can be any Gaussian integer; the value of $d$ which minimizes the number of knight moves is $d=Cint((2i-5)g/10)$, where $Cint$ is the closest Gaussian integer of the argument. The first term yields the requisite counterclockwise moves, and the second term yields the requisite clockwise moves. The real and imaginary parts of the counterclockwise and clockwise coefficients together yield the total minimum requisite moves of the chess knight, while simultaneously specifying all minimal paths.

N.B. Reply to amass.jack

I am an originator of this formula. I do not yet know of any prior publication. However, I have generated lecture notes for my talk at Acacia Creek to be given on the third Wednesday in February, as well as additional notes with numerous worked examples. These notes set forth the fundamental theory providing a foundation for the formula. Robert Word, Ph.D.

I have encountered difficulties in activating the present interface to post a reply to you, for unclear reasons. Hence, I copied the same reply into a number of related Facebook groups dealing with Mathematics, in case you might happen across them.

Robert Wordar
  • 53
  • 1
  • 2
5

The easiest way to solve this problem is to greedily move in the best direction until you get within 100 squares or so, and then A* from there.

Figuring out exactly how close you can get before you have to switch to A* is an interesting problem, but 100 squares away will surely be fine, and A* from there fits into a reasonable amount of memory.

Also note that the greedy portion will only involve 1 or 2 types of moves, and it's not hard to figure out how many of each type you will do without making an individual decision for each one.

EDIT: Since this is the CS stack I guess I should prove that it works. OK:

By symmetry I only need to consider two cases:

Case 1 -- 2 >= dx/dy >= 1/2

In this case, the greedy portion will choose between move near the diagonal. Each move reduces the manhattan distance to target by 3, and all other moves will decrease the manhattan distance by at best 1.

Lets say the length of the longest path of greedy moves is N. That path will get to a position at most 3 moves away from the target, so the best path length is at most N+3.

Now, if I use only N-m moves from the greedy path, then I will be left at a position at manhattan distance at least 3m, and it will take at least 3m moves to correct that, so the best achievable path length would be N-m+3m. If that is gonna be < N+3, then m < 2, so the shortest path includes at least N-1 moves from a longest greedy path.

Case 2 -- 1/2 >= dx/dy >= -1/2:

In this case, the greedy portion will consist of moves near the horizontal. Each greedy move will reduce the x distance by 2, and all other moves will reduce it by at most 1.

Again the longest greedy path (length N) will get within 3 moves. If we make N-m greedy moves, we are left at x distance at least 2m, and require 2m moves to fix it. If the best path consists of only N-m greedy moves, we need N+m < N+3, so the bast path can have at least N-2 elements from the longest greedy path.

Conclusion:

We can do much better than getting within 100 squares. Calculate the moves in the longest greedy path, and remove 2 moves of each type (up to the total number of moves of that type, if it contains less), and we will be left with only moves that can be part of a shortest path.

That will get us within 8 squares, and A* from there will not be expensive.

Matt Timmermans
  • 514
  • 3
  • 8
2

thanks to matt timmermans by his hint I realized for infinite chess boards no search algorithm BFS, DFS, A*, Dijkstra should be used. just calculate diagonal symmetry and imagine that start point as (0,0). just 2 corners should be hardcoded. adopted from here

System.out.println((int) distance(ENDx - STARTx, ENDy - STARTy));    
public static double distance(int x, int y) {
        // axes symmetry
        x = Math.abs(x);
        y = Math.abs(y);
        // diagonal symmetry
        if (x < y) {
            int t = x;
            x = y;
            y = t;
        }
        // 2 corner cases
        if (x == 1 && y == 0) {
            return 3;
        }
        if (x == 2 && y == 2) {
            return 4;
        }

        // main formula
        int delta = x - y;
        if (y > delta) {
            return (delta - 2 * Math.floor((float) (delta - y) / 3));
        } else {
            return (delta - 2 * Math.floor((delta - y) / 4));
        }
    }
Amir-Mousavi
  • 268
  • 2
  • 13
1

It seems there is an article from 1988 that solves this exact problem named "Knight's distance in digital geometry" Basically, the algorithm is as follows:

def Knight_dist(start,end):
    x=abs(start[0]-end[0])
    y=abs(start[1]-end[1])
    x1=max(x,y)
    x2=min(x,y)
    if (x1,x2)==(1,0):return 3
    if (x1,x2)==(2,2):return 4
    d=max((x1+1)//2, (x1+x2+2)//3) + ((x1+x2)-max((x1+1)//2, (x1+x2+2)//3))%2
    return d

But it is noteworthy to remember that this distance is the shortest possible assuming an infinite board (without the limitation of not being able to go beyond borders) and also assuming no obstacles. I think it is mostly useful for a heuristic rather than an actual distance in some applications.

0

How far does it scale currently? If you get close, some tweaks may help. If you approach doesn't scale at all (which I suspect), you'll need to reconsider the approach in general.

Some ideas:

  1. BFS uses more memory than DFS (e.g. A*), as you need to keep more data in memory. For this problem, you could prioritize moves that go into the general direction.

  2. String keys seem to be quite wasteful. Consider using a specialized data structure for sparse 2d arrays (I have implemented one here, you'll probably find more / better ones on the net). Or use a Position2D hash key.

  3. Do you need all this data in Position?

  4. Can you exploit the structure of the moves in some way? Are there some constraints that you can exploit? Do you need the full grid?