How to optimize Knight's tour algorithm?
Asked Answered
S

4

21

I code the Knight's tour algorithm in c++ using Backtracking method. But it seems too slow or stuck in infinite loop for n > 7 (bigger than 7 by 7 chessboard).

The question is: What is the Time complexity for this algorithm and how can I optimize it?!


The Knight's Tour problem can be stated as follows:

Given a chess board with n × n squares, find a path for the knight that visits every square exactly once.

Here is my code:

#include <iostream>
#include <iomanip>

using namespace std;

int counter = 1;
class horse {
  public:
    horse(int);
    bool backtrack(int, int);
    void print();
  private:
    int size;
    int arr[8][8];
    void mark(int &);
    void unmark(int &);
    bool unvisited(int &);
};

horse::horse(int s) {
    int i, j;
    size = s;
    for (i = 0; i <= s - 1; i++)
        for (j = 0; j <= s - 1; j++)
            arr[i][j] = 0;
}

void horse::mark(int &val) {
    val = counter;
    counter++;
}

void horse::unmark(int &val) {
    val = 0;
    counter--;
}

void horse::print() {
    cout << "\n - - - - - - - - - - - - - - - - - -\n";
    for (int i = 0; i <= size - 1; i++) {
        cout << "| ";
        for (int j = 0; j <= size - 1; j++)
            cout << setw(2) << setfill ('0') << arr[i][j] << " | ";
        cout << "\n - - - - - - - - - - - - - - - - - -\n";
    }
}

bool horse::backtrack(int x, int y) {
    if (counter > (size * size))
        return true;

    if (unvisited(arr[x][y])) {
        if ((x - 2 >= 0) && (y + 1 <= (size - 1))) {
            mark(arr[x][y]);
            if (backtrack(x - 2, y + 1))
                return true;
            else
                unmark(arr[x][y]);
        }
        if ((x - 2 >= 0) && (y - 1 >= 0)) {
            mark(arr[x][y]);
            if (backtrack(x - 2, y - 1))
                return true;
            else
                unmark(arr[x][y]);
        }
        if ((x - 1 >= 0) && (y + 2 <= (size - 1))) {
            mark(arr[x][y]);
            if (backtrack(x - 1, y + 2))
                return true;
            else
                unmark(arr[x][y]);
        }
        if ((x - 1 >= 0) && (y - 2 >= 0)) {
            mark(arr[x][y]);
            if (backtrack(x - 1, y - 2))
                return true;
            else
                unmark(arr[x][y]);
        }
        if ((x + 2 <= (size - 1)) && (y + 1 <= (size - 1))) {
            mark(arr[x][y]);
            if (backtrack(x + 2, y + 1))
                return true;
            else
                unmark(arr[x][y]);
        }
        if ((x + 2 <= (size - 1)) && (y - 1 >= 0)) {
            mark(arr[x][y]);
            if (backtrack(x + 2, y - 1))
                return true;
            else
                unmark(arr[x][y]);
        }
        if ((x + 1 <= (size - 1)) && (y + 2 <= (size - 1))) {
            mark(arr[x][y]);
            if (backtrack(x + 1, y + 2))
                return true;
            else
                unmark(arr[x][y]);
        }
        if ((x + 1 <= (size - 1)) && (y - 2 >= 0)) {
            mark(arr[x][y]);
            if (backtrack(x + 1, y - 2))
                return true;
            else
                unmark(arr[x][y]);
        }
    }
    return false;
}

bool horse::unvisited(int &val) {
    if (val == 0)
        return 1;
    else
        return 0;
}

int main() {
    horse example(7);
    if (example.backtrack(0, 0)) {
        cout << " >>> Successful! <<< " << endl;
        example.print();
    } else
        cout << " >>> Not possible! <<< " << endl;
}

output for the example (n = 7) above is like this:

enter image description here

Sketch answered 6/10, 2013 at 21:20 Comment(11)
You may be interested in this article about constructing large tours.Earwitness
@PeterdeRivaz thanks , it is interesting , but can't get enough help from that , the implementation sounds too complexed.Sketch
The knight's tour for a general graph is NP-hard, it's equivalent to the Hamiltonian path problem of visiting every vertex of a graph. However, for the special case of a 8x8 standard chessboard there are known linear-time algorithms. One such algorithm is described here: dl.acm.org/citation.cfm?id=363463. Of interest is that in the expected case, the greedy heuristic of 'We move the knight so that we always proceed to the square from which the knight will have the fewest onward moves' actually performs very well in practice: en.wikipedia.org/wiki/Knight's_tour#Warnsdorff.27s_ruleAdiell
I should make clear it's not equivalent to the Hamiltonian path problem but there is a reduction of the Hamiltonian path problem to the knight's tour problem which makes the knight's tour NP-hard.Adiell
I wrote a Knights Tour for an 8x8 board which ran on a P100 via a brute force and that average 18 hours for the tours the longest of which took nearly 33 hours. How long are yours taking on what system?Yeomanry
@Yeomanry wow. I did not wait much , I need it to give result very faster than this.Sketch
Note that you initialize your board (arr[][]) to 8x8. Check your boundaries, and initialize it dynamically?Chucklehead
Note that backtrack checks each of the 8 possible moves, and then recursively calls backtrack for each move that stays on the board. A 7x7 board has 49 spaces, you start at 1 space, and have upto 8 moves to check for each call of backtrack, which then have upto 8 moves to check... guess the O() complexity.Chucklehead
I can not wait the 8 chess board run :( Just think there is some errors in my program but when i retest with 5, 6 or 7 it runs successfully. May be 8 is too larger :(Sil
@Sil yes, it is in a much higher order of algorithm time.Sketch
Related: #21512183Withindoors
B
4

Since at each step you have 8 possibilities to check and this has to be done for each cell (minus the last one) the time complexity of this algorithm is O(8^(n^2-1)) = O(8^(n^2)) where n is the number of squares on the edges of the checkboard. To be precise this is the worst case time complexity (time taken to explore all the possibilities if none is found or if it is the last one).

As for the optimizations there can be 2 types of improvements:

Implementation improvements

You're calculating x-2, x-1, x+1, x+2 and the same for y at least the double of the times. I can suggest to rewrite things like this:

int sm1 = size - 1;
int xm2 = x - 2;
int yp1 = y + 1;
if((xm2 >= 0) && (yp1 <= (sm1))){
    mark(arr[x][y]);
    if(backtrack(xm2, yp1))
        return true;
    else
        unmark(arr[x][y]);
}

int ym1 = y-1;
if((xm2 >= 0) && (ym1 >= 0)){
    mark(arr[x][y]);
    if(backtrack(xm2, ym1))
        return true;
    else
        unmark(arr[x][y]);
}

note the reusing of precalculated values also in subsequent blocks. I've found this to be more effective than what I was especting; meaning that variable assignment and recall is faster than doing the operation again. Also consider saving sm1 = s - 1; and area = s * s; in the constructor instead of calculating each time.

However this (being an implementation improvement and not an algorithm improvement) will not change the time complexity order but only divide the time by a certain factor. I mean time complexity O(8^(n^2)) = k*8^(n^2) and the difference will be in a lower k factor.

Algorithm improvements

I can think this:

  • for each tour starting on in a cell in the diagonals (like starting in (0,0) as in your example) you can consider only the first moves being on one of the two half checkboards created by the diagonal.
    • This is beacouse of the simmetry or it exists 2 simmetric solutions or none.
    • This gives O(4*8^(n^2-2)) for that cases but the same remains for non simmetric ones.
    • Note that again O(4*8^(n^2-2)) = O(8^(n^2))
  • try to interrupt the rush early if some global condition suggests that a solution is impossible given the current markings.
    • for example the horse cannot jump two bulk columns or rows so if you have two bulk marked columns (or rows) and unmarked cells on both sides you're sure that there will be no solution. Consider that this can be checked in O(n) if you mantain number of marked cells per col/row updated. Then if you check this after each marking you're adding O(n*8^(n^2)) time that is not bad if n < = 8. Workaround is simply not to check alwais but maybe every n/8 markings (checking counter % 8 == 4 for example or better counter > 2*n && counter % 8 == 4
  • find other ideas to cleverly interrupt the search early but remember that the backtrack algorithm with 8 options will always have its nature of being O(8^(2^n)).

Bye

Buffybuford answered 7/10, 2013 at 18:40 Comment(0)
R
3

Here is my 2 cents. I started with the basic backtracking algorithm. It was waiting indefinitely for n > 7 as you mentioned. I implemented warnsdorff rule and it works like a magic and gives result in less than a second for boards of sizes till n = 31. For n >31, it was giving stackoverflow error as recursion depth exceeded the limit. I could find a better discussion here which talks about problems with warnsdorff rule and possible further optimizations.

Just for the reference, I am providing my python implementation of Knight's Tour problem with warnsdorff optimization



    def isValidMove(grid, x, y):
            maxL = len(grid)-1
            if x  maxL or y  maxL or grid[x][y] > -1 :
                    return False
            return True

    def getValidMoves(grid, x, y, validMoves):
            return [ (i,j) for i,j in validMoves if isValidMove(grid, x+i, y+j) ]

    def movesSortedbyNumNextValidMoves(grid, x, y, legalMoves):
            nextValidMoves = [ (i,j) for i,j in getValidMoves(grid,x,y,legalMoves) ]
            # find the number of valid moves for each of the possible valid mode from x,y
            withNumNextValidMoves = [ (len(getValidMoves(grid,x+i,y+j,legalMoves)),i,j) for i,j in nextValidMoves]
            # sort based on the number so that the one with smallest number of valid moves comes on the top
            return [ (t[1],t[2]) for t in sorted(withNumNextValidMoves) ]


    def _solveKnightsTour(grid, x, y, num, legalMoves):
            if num == pow(len(grid),2):
                    return True
            for i,j in movesSortedbyNumNextValidMoves(grid,x,y,legalMoves):
            #For testing the advantage of warnsdorff heuristics, comment the above line and uncomment the below line
            #for i,j in getValidMoves(grid,x,y,legalMoves):
                    xN,yN = x+i,y+j
                    if isValidMove(grid,xN,yN):
                            grid[xN][yN] = num
                            if _solveKnightsTour(grid, xN, yN, num+1, legalMoves):
                                    return True
                            grid[xN][yN] = -2
            return False

    def solveKnightsTour(gridSize, startX=0, startY=0):
            legalMoves = [(2,1),(2,-1),(-2,1),(-2,-1),(1,2),(1,-2),(-1,2),(-1,-2)]
            #Initializing the grid
            grid = [ x[:] for x in [[-1]*gridSize]*gridSize ]
            grid[startX][startY] = 0
            if _solveKnightsTour(grid,startX,startY,1,legalMoves):
                    for row in grid:
                            print '  '.join(str(e) for e in row)
            else:
                    print 'Could not solve the problem..'


Rue answered 29/11, 2013 at 5:30 Comment(0)
C
2

Examine your algorithm. At each depth of recursion, you examine each of 8 possible moves, checking which are on the board, and then recursively process that position. What mathematical formula best describes this expansion?

You have a fixed board size, int[8][8], maybe you should make it dynamic,

class horse
{
    ...
    int** board; //[s][s];
    ...
};

horse::horse(int s)
{
    int i, j;
    size = s;
    board = (int**)malloc(sizeof(int*)*size);
    for(i = 0; i < size; i++)
    {
        board[i] = (int*)malloc(sizeof(int)*size);
        for(j = 0; j < size; j++)
        {
            board[i][j] = 0;
        }
    }
}

Changing your tests a little by adding a function to check that a board move is legal,

bool canmove(int mx, int my)
{
    if( (mx>=0) && (mx<size) && (my>=0) && (my<size) ) return true;
    return false;
}

Note that the mark() and unmark() are very repetitive, you really only need to mark() the board, check all legal moves, then unmark() the location if none of the backtrack() return true,

And rewriting the function makes everything a bit clearer,

bool horse::backtrack(int x, int y)
{

    if(counter > (size * size))
        return true;

    if(unvisited(board[x][y]))
    {
        mark(board[x][y]);
        if( canmove(x-2,y+1) )
        {
            if(backtrack(x-2, y+1)) return true;
        }
        if( canmove(x-2,y-1) )
        {
            if(backtrack(x-2, y-1)) return true;
        }
        if( canmove(x-1,y+2) )
        {
            if(backtrack(x-1, y+2)) return true;
        }
        if( canmove(x-1,y-2) )
        {
            if(backtrack(x-1, y-2)) return true;
        }
        if( canmove(x+2,y+1) )
        {
            if(backtrack(x+2, y+1)) return true;
        }
        if( canmove(x+2,y-1) )
        {
            if(backtrack(x+2, y-1)) return true;
        }
        if( canmove(x+1,y+2) )
        {
            if(backtrack(x+1, y+2)) return true;
        }
        if( canmove(x+1,y-2) )
        {
            if(backtrack(x+1, y-2)) return true;
        }
        unmark(board[x][y]);
    }
    return false;
}

Now, think about how deep the recursion must be to visit every [x][y]? Fairly deep, huh? So, you might want to think about a strategy that would be more efficient. Adding these two printouts to the board display should show you how many backtrack steps occured,

int counter = 1; int stepcount=0;
...
void horse::print()
{
    cout<< "counter: "<<counter<<endl;
    cout<< "stepcount: "<<stepcount<<endl;
    ...
bool horse::backtrack(int x, int y)
{
    stepcount++;
    ...

Here is the costs for 5x5, 6x6, 7x6,

./knightstour 5
 >>> Successful! <<< 
counter: 26
stepcount: 253283

./knightstour 6
 >>> Successful! <<< 
counter: 37
stepcount: 126229019

./knightstour 7
 >>> Successful! <<< 
counter: 50
stepcount: 56342

Why did it take fewer steps for 7 than 5? Think about the ordering of the moves in the backtrack - if you change the order, would the steps change? What if you made a list of the possible moves [ {1,2},{-1,2},{1,-2},{-1,-2},{2,1},{2,1},{2,1},{2,1} ], and processed them in a different order? We can make reordering the moves easier,

int moves[ ] =
{ -2,+1, -2,-1, -1,+2, -1,-2, +2,+1, +2,-1, +1,+2, +1,-2 };
...
        for(int mdx=0;mdx<8*2;mdx+=2)
        {
        if( canmove(x+moves[mdx],y+moves[mdx+1]) )
        {
            if(backtrack(x+moves[mdx], y+moves[mdx+1])) return true;
        }
        }

Changing the original move sequence to this one, and running for 7x7 gives different result,

{ +2,+1, +2,-1, +1,+2, +1,-2, -2,+1, -2,-1, -1,+2, -1,-2 };


./knightstour 7
 >>> Successful! <<< 
counter: 50
stepcount: -556153603 //sheesh, overflow!

But your original question was,

The question is: What is the Time complexity for this algorithm and how can I optimize it?!

The backtracking algorithm is approximately 8^(n^2), though it may find the answer after as few as n^2 moves. I'll let you convert that to O() complexity metric.

I think this guides you to the answer, without telling you the answer.

Chucklehead answered 7/10, 2013 at 19:18 Comment(4)
The code is for sure more readable with this approach but from a computational point of view I will sugget to avoid using the generic function canmove(x,y) because it always checks 4 conditions instead of 2 becouse it checks also in the direction where I'm coming from.Buffybuford
Agreed. Note that after refactoring the code for the canmove() function, the loop on move order was clear. And that shows how move order affects this algorithm. And then a follow-up comment about how taking moves in different order could lead to a better algorithm. Because the poster needs to look at better algorithms.Chucklehead
Right. And should be noted that for different starting positions may be that the best performing search order is not the same.Buffybuford
Yes, and looking at the Wikipedia article shows a definite 'circular' pattern to the 8x8 solution, and tweaking the algorithm to bias the move direction to follow the edges (clockwise or counterclockwise) would converge to a solution much faster.Chucklehead
D
0

this is a new solution:

in this method, using the deadlock probability prediction at the next movement of the knight in the chessboard, a movement will be chose that it’s tending to the deadlock probability is less than the other ones, we know at the first step this deadlock probability is zero for every cells and it will be changed gradually. The knight in the chessboard has between 2 and 8 moves, so each cells has predetermined value for next move.

Selecting the cells that have less available movement is best choice because it will tend to the deadlock in the future unless it is filled. There is an inverse relationship between allowed movement number and reach an impasse. the outer cells is in the highest priority, As regards in a knight's tour problem the knight has to cross a cell only once, these value will be changed gradually in future travels. Then in the next step a cell will be chose that has these conditions

  1. The number of its adjacent empty cells is less than others, or in the other words the probability to be filled is more
  2. After selecting, the adjacent houses doesn’t going to deadlock

you can read my full article about this problem here Knight tour problem article

and you can find the full source from here Full Source in GitHub

I hope it will be useful

Doubtless answered 23/9, 2020 at 17:5 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.