如何解决具有特殊约束的部分骑士之旅
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。
【讨论】:
以上是关于如何解决具有特殊约束的部分骑士之旅的主要内容,如果未能解决你的问题,请参考以下文章