A chess knight's moves

So, the language and Soviet math contest kind of this problem suggested me to search it in Russian Internet. I traced its origins back to XXXIII Ural tournament of young mathematicians, where it was presented at 2009, February 27 at so-called math fight and was attributed to A. Adel’shin (А. Адельшин). Problems for smaller boards can be found here (both pages are of course in Russian). A positive solution was found in Russian math magazine “Kvantik” (a small quantum) from 2014, August:

enter image description here


Not satisfied with a single solution, I ran a python script to enumerate all possible solutions. It produced the following 9 tours (28 tours before discarding symmetric ones).

# +-#-+-+ +-# +-#-+-+
| |     | | | |     |
+ + +-+-# + + + +-+-#
| | |     | | | |    
+-# # #-+ # +-# # #-+
    | | | |     | | |
#-+-+ + + + #-+-+ + +
|     | | | |     | |
+-+-#-+ #-+ +-+-#-+ #

# +-#-+-+ +-#-+-+ #-+
| |     | |     | | |
+ + +-+-# + +-+-# + +
| | |     | |     | |
+-# # #-+ # # +-#-+ #
    | | | | | |     |
#-+-+ + + + + + #-+-+
|     | | | | | |    
+-+-#-+ #-+ +-# +-+-#

# +-#-+-+ +-+-#-+ #-+
| |     | |     | | |
+ + +-+-# #-+-+ + + +
| | |         | | | |
+-# # #-+ #-+ # #-+ #
    | | | | | |     |
#-+-+ + + + + + #-+-+
|     | | | | | |    
+-+-#-+ #-+ #-+ +-+-#

#-+-+ #-+ +-#-+-+ #-+
    | | | |     | | |
+-+-# + + + +-+-# + +
|     | | | |     | |
# +-#-+ # # # +-#-+ #
| |     | | | |     |
+ + #-+-+ + + + #-+-+
| | |     | | | |    
+-# +-+-#-+ +-# +-+-#

#-+-+ #-+ +-+-#-+ #-+
    | | | |     | | |
+-+-# + + #-+-+ + + +
|     | |     | | | |
# +-#-+ # #-+ # #-+ #
| |     | | | |     |
+ + #-+-+ + + + #-+-+
| | |     | | | |    
+-# +-+-#-+ #-+ +-+-#

#-+-+ +-# +-+-#-+ #-+
    | | | |     | | |
+-+-# + + #-+-+ + + +
|     | |     | | | |
# +-# # +-#-+ # #-+ #
| | | |     | |     |
+ + + +-+-# + + #-+-+
| | |     | | | |    
+-# +-#-+-+ #-+ +-+-#

#-+-+ +-#-+-+ +-#-+-+
    | |     | |     |
+-+-# + +-+-# + +-+-#
|     | |     | |    
# +-# # #-+-+ # #-+-+
| | | |     | |     |
+ + + +-+-# # +-+-# #
| | |     | |       |
+-# +-#-+-+ +-+-#-+-+

#-+-+ +-#-+-+ +-+-#-+
    | |     | |     |
+-+-# + +-+-# # +-# +
|     | |     | | | |
# +-# # #-+-+ + + + #
| | | |     | | | |  
+ + + +-+-# # +-# +-#
| | |     | |       |
+-# +-#-+-+ +-+-#-+-+

#-+-+ +-+-#-+ #-+ #-+
    | |     | | | | |
+-+-# #-+-+ + + + + +
|         | | | | | |
# +-# +-# # #-+ #-+ #
| | | | | |         |
+ + + + + +-+-# #-+-+
| | | | |     | |    
+-# +-# +-#-+-+ +-+-#

Here's the python script. Mostly brute-force, pruning out situations where the unvisited squares are not connected or have 2 vertices with only one adjacent unvisited square (since the knight must end his tour at such a square). Runs in under a minute.

BOARD_WIDTH = 11
BOARD_HEIGHT = 5

on_board = lambda (x,y): 0 <= x < BOARD_WIDTH and 0 <= y < BOARD_HEIGHT

adjacent = lambda (x,y): [p for p in [(x-1,y),(x+1,y),(x,y-1),(x,y+1)] if on_board(p)]

VERTICES = set((x,y) for x in range(BOARD_WIDTH) for y in range(BOARD_HEIGHT))

def is_admissible(cur, visited):
    # count "alcoves", places the knight must end his tour
    # if there are more than one, a tour doesn't exist.
    alcove_count = 0
    for v in VERTICES:
        if v in visited: continue

        # squares next to the knight don't count as alcoves
        if abs(cur[0]-v[0]) + abs(cur[1]-v[1]) == 1: continue

        deg = len(filter(lambda p:not p in visited, adjacent(v)))
        if deg == 1:
            alcove_count += 1
            if alcove_count > 1:
                # print 'too many alcoves.'
                return False

    unvisited = VERTICES - visited
    for u in unvisited:
        break

    found = [u]
    unvisited.remove(u)

    while found:
        u = found.pop()
        for v in adjacent(u):
            if v in unvisited and not v in visited and not v in found:
                found.append(v)
                unvisited.remove(v)
    if unvisited:
        # print 'not connected.'
        return False

    return True

def search(path):
    global longest_so_far

    if len(path) == len(VERTICES):
        yield path
        return


    cur = path[-1]
    visited = set(path)

    if not is_admissible(cur, visited):
        return

    for L in [
        [(1,0),(2,0),(2,1)],
        [(1,0),(2,0),(2,-1)],
        [(1,0),(1,1),(1,2)],
        [(1,0),(1,-1),(1,-2)],

        [(-1,0),(-2,0),(-2,1)],
        [(-1,0),(-2,0),(-2,-1)],
        [(-1,0),(-1,1),(-1,2)],
        [(-1,0),(-1,-1),(-1,-2)],

        [(0,1),(0,2),(1,2)],
        [(0,1),(0,2),(-1,2)],
        [(0,1),(1,1),(2,1)],
        [(0,1),(-1,1),(-2,1)],

        [(0,-1),(0,-2),(1,-2)],
        [(0,-1),(0,-2),(-1,-2)],
        [(0,-1),(1,-1),(2,-1)],
        [(0,-1),(-1,-1),(-2,-1)],
    ]:
        new_path = [(l[0]+cur[0], l[1]+cur[1]) for l in L]
        if all(on_board(p) and not p in visited for p in new_path):
            for solution in search(path + new_path):
                yield solution

def path_to_string(path):
    chars = [[' ']*(BOARD_WIDTH*2-1) for _ in range(BOARD_HEIGHT*2-1)]

    for i,(x,y) in enumerate(path):
        chars[2*y][2*x] = '#' if i % 3 == 0 else '+'

    for i in range(len(path)-1):
        (x0,y0),(x1,y1) = path[i:i+2]
        chars[y0+y1][x0+x1] = '|' if x0 == x1 else '-'

    return '\n'.join(''.join(row) for row in chars)

def canonize_path_string(s):
    a = s
    b = s[::-1]
    c = '\n'.join(s.split('\n')[::-1])
    d = c[::-1]

    return min(a,b,c,d)

path_strings = set()
for v in VERTICES:
    for path in search([v]):
        path_strings.add(canonize_path_string(path_to_string(path)))

print '\n\n'.join(sorted(path_strings))

Edit: I also ran it on an 8x8 board. This took much longer and produced the following 52 solutions: http://pastebin.com/pThG6PsD