7
votes

I know there are several similar threads, but I didn't find a solution even outside of SO. Here's my problem: I implemented Warnsdorff's algorithm for the Knight's Tour problem http://en.wikipedia.org/wiki/Knight%27s_tour , but it doesn't give a solution in some cases. On some places I read it can work much better with some alterations, but nobody specifies which alterations are those. Does somebody know the solution? I know of other algorithms, but they are much more complex.

It sometimes doesn't give a good solution even for a 8x8 chessboard. I think no point in reading through my code, since it's a classical Warnsdorff's: check possible moves, and choose the one with the least possible moves in the next step.

4
What exactly are you asking? How to improve Warnsdorff's rule? Wikipedia states that Warnsdorff's rule should give a solution for any starting square. Also the Warnsdorff rule does not give much room for improvement; What specifically are you thinking of?Captain Giraffe
@CaptainGiraffe it isn't perfect, really. I've just found this: mirran.web.surftown.se/knight/bWarnsd.htm and trying to see if it's a real improvement. The part, when you have more than 1 possible move, with equal quality. This improvement says, choose the one farthest from the center of the board. Will see when I implement it.Mate E.
I see. The wiki article is quite misleading in its formulation. Yes, I am familiar with the "farthest square" in tiebreaks. I'm still not sure what you are asking though.Captain Giraffe
I'm asking for an improvement on this algorithm that gives good solutions in more cases than the classical one. Something that people already tried and works better. Like the link I provided in the previous edit. Yeah, the wiki is misleading. If I solve the problem with the solution from the link above, I'll ask the admin to close this question.Mate E.
In github.com/douglassquirrel/warnsdorff/blob/master/… an improved Warnsdorff algorithm is presented, which claims to be perfect for a chessboard > 112Gunther Piez

4 Answers

7
votes

The link for W+ no longer exist, making the accepted answer not usable.

Improvements to the Warnsdorff algorithm are:

Arnd Roth's proposition: Roth decided that the problem in Warnsdorff's heuristic, lays in the random tiebreak rule. The proposition is to break the ties by choosing the successor with the largest euclidean distance from the center of the board.

The problem here is what happens when more than one successor share the same distance.
Arnd Roth claimed that this improvement first failed on a board with 428 rows and had less than 1% failures on all boards with fewer than 2000 rows.

Ira Pohl's proposition: To break the ties Pohl decided to apply Warnsdorff's rule a second time. To achieve this we must take the sum of the degrees of all unvisited neighbors, of the successors that tied, and choose the square whose sum is minimal.

One of the problems here is that no matter how many times we apply Warnsdorff's rule we will result in a tie (between the two adjacent squares) if we start in the corner square. Furthermore if we apply Warnsdorff's heuristic a large number of times then asymptotically this is equal to an exhaustive search.

Pohl also suggested, if we fail to produce a successor after applying Warnsdorff for the 2nd time, to break ties by using a fixed arbitrary ordering of the squares. His claim is that this successfully produces a solution on a 8*8 board.

By using one of these enhancements we have better chances of creating a solution systematically by preventing possible lock-ins.

6
votes

Here's what I found out:

Please note that this still isn't a definitive answer and I ain't no graph theory expert, so these are observations only.

I'll call the classic Warnsdorff heuristic "W". The improvement from http://mirran.web.surftown.se/knight/bWarnsd.htm (Cached: http://web.archive.org/web/20120213164632/http://mirran.web.surftown.se/knight/bWarnsd.htm) will be called "W+". The improvement from https://github.com/douglassquirrel/warnsdorff/blob/master/5_Squirrel96.pdf?raw=true will be "W2". The number of horizontal fields will be "x" and the vertical will be "y".

So, here are my observations.

The short version:

W is simple, but on many occasions it can't provide a solution. That triggered this question at first. W+ is simple too and gives a big improvement, especially on large boards. W2 is much more complex to implement, and compared to W+ it doesn't seem to give much better results. So I vote for W+. Anyway, that's the variant I'll use.

The long version:

W

advantages: Compared to other Knights Tour algorithms, simplicity. Compared to W+, it doesn't really have advantages. Compared to W2, it's much more easy to implement.

disadvantages: there are plenty of cases when there is a solution, but W can't provide one it tends to mess up with bigger boards (50+)

W+

advantages: Compared to other Knights Tour algorithms, simplicity. Compared to W: it can provide a solution in much more cases and it almost isn't more complex than W. Compared to W2, it's much more easy to implement and W+ works on non-square boards too. 10x20 for example.

disadvantages: Compared to W, it doesn't have disadvantages. Compared to other knights tour algorithms, is that this one can get stuck in some cases. The toughest for W+ are small boards like 5x5, 6x6, 7x7, 9x9 etc. As stated on the Wiki, it has problems with boards when both x and y are even. On the other hand, when x and y are even, but greater than 9 it seems W+ manages to find a solution. Compared to W2, I didn't experience disadvantages.

W2

advantages: Compared to W, it gives solutions in much more cases, especially for large boards. Compared to W+ I didn't notice advantages.

disadvantages: Implementation compared to W and W+.

Conclusion:

My opinion is that W+ is practically the most acceptable. Don't forget that it isn't perfect. And I have to say, that my implementation doesn't allow for really big boards. I tested W+ up to 90x90 (8100 nodes) and it still provided solutions. Although, I didn't do extensive testing because of limited time. I hope this helps someone who confronted this problem before. Because this isn't a definite answer, I won't accept it for a while, in hope that someone appears who can give a complete answer. Sorry for the long read.

1
votes

Here is a working example in Python 2 that uses a timer to go through the board and illustrates the solution. I find the node closest to the edge of the board for tie breakers, and if both are same, I just return the first. This seemed a natural choice since cells closer to the edge should have less possible moves if the board is empty. Seems to be working well, but as Panos mentions, there is a 1% fail rate with Arnd Roth's proposition. This code can be easily modified to use Ira Pohl's Proposition. Visit node can be modified to run possible_unvisited_moves again against the nodes that both had the minimum moves. In case of ties, using the first should work.

class GNode(object):
    """ Used to represent a node in the graph """
    def __init__(self, value, row=None, col=None):
        self.row = row  # allows node to know its loc. in array
        self.col = col
        self.value = value

def setup_graph(n):
    """ Create x by x grid (graph inside an array). """
    graph = []
    for row in range(n):
        graph.append([])
        for col in range(n):
            graph[row].append(GNode(None,row=row, col=col))
    return graph

def possible_moves(graph_node, total):
    """ Find out all possible moves for the current node position. """
    moves = []
    move_patterns = [[-1,-2], [-1, 2], [-2, 1], [-2, -1], [1, -2], [1, 2], [2, 1], [2, -1]]
    for row, col in move_patterns:
        move_row = graph_node.row + row; move_col = graph_node.col + col
        if move_row >= 0 and move_col >= 0 and move_row < total and move_col < total:
            moves.append([move_row, move_col])
    return moves

def possible_unvisited_moves(graph_node, grid):
    """Get all possible moves and weed out the ones that are already visited. 
       visited means they have a value.
    """
    moves = possible_moves(graph_node, len(grid))
    unvisited = []
    for move in moves:
        if grid[move[0]][move[1]].value is None:
            unvisited.append(move)
    return unvisited


def closest_to_edge(pos1, pos2, total):  
    """ Find out which position is closest to the edge of board, and return it. 
        pos are 2 arrays [x,y], total is the board size (length)
    """
    total -= 1
    pos1_min = min(total - pos1[0], pos1[0], pos1[1], total-pos1[1])
    pos2_min = min(total - pos2[0], pos2[0], pos2[1], total-pos2[1])
    if pos2_min < pos1_min: return pos2
    return pos1  # default


def visit_node(graph_node, graph):
    """ Check possible unvisited moves from the pos & find next move to make """
    graph_node.value = "{}{}".format(graph_node.row, graph_node.col)  # visited
    p_moves = possible_unvisited_moves(graph_node, graph)
    next_move = None
    min_no_moves = 9 # highest can be 8 for knights move pattern
    for move in p_moves:
        next_moves = possible_unvisited_moves(graph[move[0]][move[1]], graph)
        if len(next_moves) < min_no_moves:
            next_move = move
            min_no_moves = len(next_moves)
        elif len(next_moves) == min_no_moves:
            min_move = closest_to_edge(next_move, move, len(graph))
            if min_move != next_move:
                next_move = min_move
                min_no_moves = len(next_moves)
    if next_move:
        os.system(clear_screen) 
        print "This Move: [",graph_node.row, ",",  graph_node.col, "]. Next Move: ", next_move, "Min Moves from next: ", min_no_moves
        display_graph(graph)
        import time
        time.sleep(.5)
        return visit_node(graph[next_move[0]][next_move[1]], graph)
    else:
        os.system(clear_screen) 
        display_graph(graph)
        return "Done"

def display_graph(graph):
    print ""
    for row in graph:
        row_vals = []
        for cell in row:
            row_vals.append(cell.value)
        print row_vals

import os
clear_screen = 'cls' if os.name == 'nt' else 'clear'

graph = setup_graph(8)

print visit_node(graph[4][4], graph)

Have fun playing with it. :)

1
votes

I think what is most overlooked when applying Warnsdorff's Rule is that the path being constructed has two ends. Considerably better results are achieved when applying a two level tie break rule to both ends of the path. And, as an added bonus, the number of reentrant tours produced is greatly increased.