如何解决具有特殊约束的部分骑士之旅

Posted

技术标签:

【中文标题】如何解决具有特殊约束的部分骑士之旅【英文标题】:How to solve partial Knight's Tour with special constraints 【发布时间】:2021-11-14 19:15:51 【问题描述】:

这里的图片中描述的骑士之旅问题,带有图表。

A knight was initially located in a square labeled 1. It then proceeded to make a
series of moves, never re-visiting a square, and labeled the visited squares in
order. When the knight was finished, the labeled squares in each region of connected
squares had the same sum.

A short while later, many of the labels were erased. The remaining labels can be seen
above.

Complete the grid by re-entering the missing labels. The answer to this puzzle is
the sum of the squares of the largest label in each row of the completed grid, as in
the example.

[1]: E.g. the 14 and 33 are in different regions.

图片解释得更清楚了,但总的来说,骑士绕着 10 x 10 的网格走。图片显示了一个 10 x 10 的板,显示了一些已进入的位置,以及它的旅程的哪个点。你不知道骑士从哪个位置开始,或者它做了多少动作。

板上的彩色组需要全部加起来相同的数量。

我已经构建了一个 python 求解器,但它运行了很长时间 - 使用递归。我注意到一个组的最大和是 197,基于有 100 个方格,最小的组是 2 个相邻的方格。

我在这个链接的代码:https://pastebin.com/UMQn1HZa

import sys, numpy as np
 
fixedLocationsArray = [[ 12, 0,  0,  0,  0,  0, 0,  0,  0,  0],
                 [ 0,  0,  0,  0,  0,  0,  5,  0,  23, 0],
                 [ 0,  0,  0,  0,  0,  0,  8,  0,  0, 0],
                 [ 0,  0,  0,  14, 0,  0,  0,  0,  0,  0],
                 [ 0,  0,  0,  0,  0,  0,  0,  0,  0, 0],
                 [ 0,  2,  0,  0,  0,  0,  0,  0, 0, 0],
                 [ 0,  0,  0,  0,  20,  0,  0,  0,  0,  0],
                 [ 0,  0,  0,  0,  33,  0,  0,  0,  0,  0],
                 [ 0,  0,  0,  0,  0,  0,  0,  0, 0,  0],
                 [ 0,  0,  0,  0,  0,  0,  0,  0,  0,  28]]
 
groupsArray = [[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
                  [0, 0, 0, 1, 0, 0, 0, 0,10, 0],
                  [0, 0, 0, 1, 0, 0, 0, 0,10, 0],
                  [0, 0, 1, 1, 1, 1, 9,10,10,10],
                  [2, 0, 1, 0, 0,11, 9, 9, 9, 9],
                  [2, 0, 0, 0,11,11,11,15,15, 9],
                  [2, 4, 4,14,11,12,12,15,15, 8],
                  [2, 3, 4,14,14,13,13,13,15, 8],
                  [2, 3, 5,14,16,16,16, 7, 7, 8],
                  [3, 3, 5, 6, 6, 6, 6, 6, 7, 8]]
 
'''
Solver
- Noted that the maximum sum of a group is 197 since the group of only 2 can have the 100 filled and then 97 on return
'''
 
class KnightsTour:
    def __init__(self, width, height, fixedLocations, groupsArray):
        self.w = width
        self.h = height
        self.fixedLocationsArray = fixedLocations
        self.groupsArray = groupsArray
        self.npfixedLocationsArray = np.array(fixedLocations)
        self.npgroupsArray = np.array(groupsArray)
 
        self.board = [] # Contains the solution
        self.generate_board()
 
    def generate_board(self):
        """
        Creates a nested list to represent the game board
        """
        for i in range(self.h):
            self.board.append([0]*self.w)
 
    def print_board(self): # Prints out the final board solution
        print("  ")
        print("------")
        for elem in self.board:
            print(elem)
        print("------")
        print("  ")
 
    def generate_legal_moves(self, cur_pos, n):
        """
        Generates a list of legal moves for the knight to take next
        """
        possible_pos = []
        move_offsets = [(1, 2), (1, -2), (-1, 2), (-1, -2),
                        (2, 1), (2, -1), (-2, 1), (-2, -1)]
 
        locationOfNumberInFixed = [(ix,iy) for ix, row in enumerate(self.fixedLocationsArray) for iy, i in enumerate(row) if i == n+1]
        groupsizeIsNotExcessive = self.groupsNotExcessiveSize(self.board, self.groupsArray)
 
        for move in move_offsets:
            new_x = cur_pos[0] + move[0]
            new_y = cur_pos[1] + move[1]
            new_pos = (new_x, new_y)
 
            if groupsizeIsNotExcessive:
                if locationOfNumberInFixed:
                    print(f"This number n+1 exists in the fixed grid at locationOfNumberInFixed[0]")
                    if locationOfNumberInFixed[0] == new_pos:
                        print(f"Next position is new_pos and matches location in fixed")
                        possible_pos.append((new_x, new_y))
                    else:
                        continue
                elif not locationOfNumberInFixed: # if the current index of move is not in table, then evaluate if it is a legal move
                    if (new_x >= self.h): # if it is out of height of the board, continue, don't app onto the list of possible moves
                        continue
                    elif (new_x < 0):
                        continue
                    elif (new_y >= self.w):
                        continue
                    elif (new_y < 0):
                        continue
                    else:
                        possible_pos.append((new_x, new_y))
            else:
                continue
        
        print(f"The legal moves for index n are possible_pos")
        print(f"The current board looks like:")
        self.print_board()
 
        return possible_pos
 
    def sort_lonely_neighbors(self, to_visit, n):
        """
        It is more efficient to visit the lonely neighbors first, 
        since these are at the edges of the chessboard and cannot 
        be reached easily if done later in the traversal
        """
        neighbor_list = self.generate_legal_moves(to_visit, n)
        empty_neighbours = []
 
        for neighbor in neighbor_list:
            np_value = self.board[neighbor[0]][neighbor[1]]
            if np_value == 0:
                empty_neighbours.append(neighbor)
 
        scores = []
        for empty in empty_neighbours:
            score = [empty, 0]
            moves = self.generate_legal_moves(empty, n)
            for m in moves:
                if self.board[m[0]][m[1]] == 0:
                    score[1] += 1
            scores.append(score)
 
        scores_sort = sorted(scores, key = lambda s: s[1])
        sorted_neighbours = [s[0] for s in scores_sort]
        return sorted_neighbours
 
    def groupby_perID_and_sum(self, board, groups):
 
        # Convert into numpy arrays
        npboard = np.array(board)
        npgroups = np.array(groups)
 
        # Get argsort indices, to be used to sort a and b in the next steps
        board_flattened = npboard.ravel()
        groups_flattened = npgroups.ravel()
 
        sidx = groups_flattened.argsort(kind='mergesort')
        board_sorted = board_flattened[sidx]
        groups_sorted = groups_flattened[sidx]
 
        # Get the group limit indices (start, stop of groups)
        cut_idx = np.flatnonzero(np.r_[True,groups_sorted[1:] != groups_sorted[:-1],True])
 
        # Create cut indices for all unique IDs in b
        n = groups_sorted[-1]+2
        cut_idxe = np.full(n, cut_idx[-1], dtype=int)
 
        insert_idx = groups_sorted[cut_idx[:-1]]
        cut_idxe[insert_idx] = cut_idx[:-1]
        cut_idxe = np.minimum.accumulate(cut_idxe[::-1])[::-1]
 
        # Split input array with those start, stop ones
        arrayGroups = [board_sorted[i:j] for i,j in zip(cut_idxe[:-1],cut_idxe[1:])]
        arraySum = [np.sum(a) for a in arrayGroups]
 
        sumsInListSame = arraySum.count(arraySum[0]) == len(arraySum)
 
        return sumsInListSame
 
    def groupsNotExcessiveSize(self, board, groups):
        # Convert into numpy arrays
        npboard = np.array(board)
        npgroups = np.array(groups)
 
        # Get argsort indices, to be used to sort a and b in the next steps
        board_flattened = npboard.ravel()
        groups_flattened = npgroups.ravel()
 
        sidx = groups_flattened.argsort(kind='mergesort')
        board_sorted = board_flattened[sidx]
        groups_sorted = groups_flattened[sidx]
 
        # Get the group limit indices (start, stop of groups)
        cut_idx = np.flatnonzero(np.r_[True,groups_sorted[1:] != groups_sorted[:-1],True])
 
        # Create cut indices for all unique IDs in b
        n = groups_sorted[-1]+2
        cut_idxe = np.full(n, cut_idx[-1], dtype=int)
 
        insert_idx = groups_sorted[cut_idx[:-1]]
        cut_idxe[insert_idx] = cut_idx[:-1]
        cut_idxe = np.minimum.accumulate(cut_idxe[::-1])[::-1]
 
        # Split input array with those start, stop ones
        arrayGroups = [board_sorted[i:j] for i,j in zip(cut_idxe[:-1],cut_idxe[1:])]
        arraySum = [np.sum(a) for a in arrayGroups]
        print(arraySum)
 
        # Check if either groups aren't too large
        groupSizeNotExcessive = all(sum <= 197 for sum in arraySum)
 
        return groupSizeNotExcessive
    
    def tour(self, n, path, to_visit):
        """
        Recursive definition of knights tour. Inputs are as follows:
        n = current depth of search tree
        path = current path taken
        to_visit = node to visit, i.e. the coordinate
        """
        self.board[to_visit[0]][to_visit[1]] = n # This writes the number on the grid
        path.append(to_visit) #append the newest vertex to the current point
        print(f"Added n")
        print(f"For n+1 visiting: ", to_visit)
 
 
        if self.groupby_perID_and_sum(self.board, self.npgroupsArray): #if all areas sum
            self.print_board()
            print(path)
            print("Done! All areas sum equal")
            sys.exit(1)
 
        else:
            sorted_neighbours = self.sort_lonely_neighbors(to_visit, n)
            for neighbor in sorted_neighbours:
                self.tour(n+1, path, neighbor)
 
            #If we exit this loop, all neighbours failed so we reset
            self.board[to_visit[0]][to_visit[1]] = 0
            try:
                path.pop()
                print("Going back to: ", path[-1])
            except IndexError:
                print("No path found")
                sys.exit(1)
                
 
if __name__ == '__main__':
    #Define the size of grid. We are currently solving for an 8x8 grid
 
    kt0 = KnightsTour(10, 10, fixedLocationsArray, groupsArray)
 
    kt0.tour(1, [], (3, 0))
    # kt0.tour(1, [], (7, 0))
    # kt0.tour(1, [], (7,2))
    # kt0.tour(1, [], (6,3))
    # kt0.tour(1, [], (4,3))
    # kt0.tour(1, [], (3,2))
 
    # startingPositions = [(3, 0), (7, 0), (7,2), (6,3), (4,3), (3,2)]
 
    kt0.print_board()

【问题讨论】:

有趣的问题。但是您应该在此处包含问题描述,而不是图像。 @DanielHao 完成(由我为 OP 完成)。 非常感谢您为我这样做,道歉本可以在第一轮更清楚地说明 - 下次注意! 【参考方案1】:

您可以包含以下一些观察结果,以便能够更早地停止回溯。

首先请记住,对于n 步骤,所有区域的总和可以使用公式n(n+1)/2 计算。这个数字必须能均匀地整除到组中,即它必须能被 17 整除,即组的数量。

此外,如果我们查看 12,我们可以得出结论,11 和 13 一定在同一个区域,因此我们得到每个区域的数字的下限为 2+5+8+11+12+13= 51.

最后,我们有大小为 2 的组,因此最大的两个步数必须构成一组的总和。

使用这些条件,我们可以计算剩余的可能步数

# the total sum is divisible by 17: 
# n*(n+1)/2 % 17 == 0 

# the sum for each group is at least the predictable sum for
# the biggest given group 2+5+8+11+12+13=51: 
# n*(n+1)/(2*17) >= 51

# since there are groups of two elements the sum of the biggest 
# two numbers must be as least as big as the sum for each group
# n+n-1 >= n*(n+1)/(2*17)
[n for n in range(101) if n*(n+1)/2 % 17 == 0 and n*(n+1)/(2*17) >= 51 and n+n-1 >= n*(n+1)/(2*17)]

给我们 [50,51]。 所以骑士必须走了 50 或 51 步,每个区域的总和必须是 75 或 78。

【讨论】:

以上是关于如何解决具有特殊约束的部分骑士之旅的主要内容,如果未能解决你的问题,请参考以下文章

我正在使用回溯解决骑士之旅问题,但我没有得到想要的结果

骑士之旅(回溯)

骑士之旅中的堆栈实现[关闭]

陷入无限循环(骑士之旅问题)

模拟骑士序列之旅

骑士之旅 - 导致无限循环,我不知道为啥