1. Problem description
The eight digit problem is also known as the nine palace problem. On a 3×3 chessboard, there are eight chess pieces, each marked with a number from 1 to 8, and the numbers marked on different pieces are different. There is also a space on the board (represented by the number 0), and pieces adjacent to the space can be moved into the space. The problem to be solved is: given an initial state and a target state, find a moving step with the least number of moving pieces from the initial state to the target state.
This problem can be solved with the A* heuristic search algorithm.
The evaluation function of the A* algorithm is as follows:
Among them, the heuristic function can choose w(n) and p(n). This article uses w(n) as an example to write a program
2. Algorithm implementation [theoretical part]
This article takes the following situation as an example for analysis:
1. Abstract the problem
①. Selection of operator
If we focus on the number, the corresponding operation operator is the movement of the number. It can be seen that there are 4 (direction)*8 (number of codes)=32 types of operators, and the design is more complicated.
If you focus on the space, the corresponding operation operator is the movement of the space. In the most ideal situation [that is, when the space is in the middle of the chessboard], there are at most 4 types of operators [move up, move down, and move left left, right], the design is relatively simple.
To sum up, this program chooses to focus on spaces, and uses the four operators of up, down, left, and right for programming
②. Abstraction of digital movement process
The distribution of numbers on the 3*3 chessboard can be abstracted into a one-dimensional array, so that each movement of the space is equivalent to exchanging the positions of two elements in the one-dimensional array , as shown in the following figure:
So far, the problem of digital movement has been transformed into the problem of finding the subscripts of the two elements to be exchanged in the array and exchanging them
2. Actual execution process
①. Search tree
From the search tree in the above figure, we can see that:
Any node on the tree should contain the following information:
The depth of the node in the tree
The evaluation function value f(n) of the node
The digital sequence of the node [abstracted as a one-dimensional array]
②.open table and closed table
It can be seen from the above table:
The open table stores the nodes sorted by the value of the evaluation function , and the closed table stores the first node taken out from the open table each time through the loop, until the number sequence of the taken out node is equal to the final number sequence, and the algorithm ends.
3. Unsolvable situation
First of all, it is clear that the eight-digit problem has no solution. When writing a program, it is first necessary to judge whether the transformation between the initial sequence and the target sequence has a solution. If the two sequences without solutions are executed, the algorithm will will be caught in an endless loop
The judgment of whether the two states have solutions is determined by the parity of the inversion numbers of the two sequences . If the inversion numbers of the two sequences are both odd or even , the transformation of the two sequences has a solution, otherwise there is no solution.
What is a reverse ordinal number? How to find reverse ordinal number? Read this article, here is not much to repeat the reverse ordinal article
4. Selection of operator
①. Boundary conditions ![](https://img-blog.csdnimg.cn/390c2effb97349e19408e1ee6053ced8.png)
②. Prevent infinite loop
The previous UP operation was performed, and the DOWN operation should be disabled in the next iteration
The previous LEFT operation was performed, the next iteration should disable the RIGHT operation,
And vice versa, the purpose is to avoid an infinite loop
The specific example is as follows:
In short, when the selection of operators does not consider the constraints in ② , the result of the selection is very fixed. For example, only DOWN and RIGHT can be selected for position 0, and only LEFT, UP and DOWN can be selected for position 5. In the actual program, the operator that can be selected this time can be obtained according to the position of the element in the array + constraints .
5. Data structure design
After the above analysis, we can see that to implement the algorithm, the key is to design the data structure of each node in the search tree. The structure of this design is as follows:
class statusObject:
def __init__(self):
# 当前状态的序列
self.array = []
# 当前状态的估价函数值
self.Fn = 0
# cameFrom表示该状态由上一步由何种operation得到
# 目的是为了过滤 【死循环】
# 0表示初始无状态 1表示up 2表示down 3表示left 4表示right
self.cameFrom = 0
# 第一次生成该节点时在图中的深度 计算估价函数使用
self.Dn = 0
# 该节点的父亲节点,用于最终溯源最终解
self.Father = statusObject
3. Algorithm implementation [code part]
1. Flowchart:
2. Program source code
The program uses the numpy package, please install it yourself before running
In addition, a lot of print statements were used to view the results during the debugging process, which has been commented, please delete it yourself if you don’t need it
import operator
import sys
import numpy as np
class statusObject:
def __init__(self):
# 当前状态的序列
self.array = []
# 当前状态的估价函数值
self.Fn = 0
# cameFrom表示该状态由上一步由何种operation得到
# 目的是为了过滤 【死循环】
# 0表示初始无状态 1表示up 2表示down 3表示left 4表示right
self.cameFrom = 0
# 第一次生成该节点时在图中的深度 计算估价函数使用
self.Dn = 0
self.Father = statusObject
def selectOperation(i, cameFrom):
# @SCY164759920
# 根据下标和cameFromReverse来选择返回可选择的操作
selectd = []
if (i >= 3 and i <= 8 and cameFrom != 2): # up操作
selectd.append(1)
if (i >= 0 and i <= 5 and cameFrom != 1): # down操作
selectd.append(2)
if (i == 1 or i == 2 or i == 4 or i == 5 or i == 7 or i == 8): # left操作
if (cameFrom != 4):
selectd.append(3)
if (i == 0 or i == 1 or i == 3 or i == 4 or i == 6 or i == 7): # right操作
if (cameFrom != 3):
selectd.append(4)
return selectd
def up(i):
return i - 3
def down(i):
return i + 3
def left(i):
return i - 1
def right(i):
return i + 1
def setArrayByOperation(oldIndex, array, operation):
# i为操作下标
# 根据operation生成新状态
if (operation == 1): # up
newIndex = up(oldIndex) # 得到交换的下标
if (operation == 2): # down
newIndex = down(oldIndex)
if (operation == 3): # left
newIndex = left(oldIndex)
if (operation == 4): # right
newIndex = right(oldIndex)
# 对调元素的值
temp = array[newIndex]
array[newIndex] = array[oldIndex]
array[oldIndex] = temp
return array
def countNotInPosition(current, end): # 判断不在最终位置的元素个数
count = 0 # 统计个数
current = np.array(current)
end = np.array(end)
for index, item in enumerate(current):
if ((item != end[index]) and item != 0):
count = count + 1
return count
def computedLengthtoEndArray(value, current, end): # 两元素的下标之差并去绝对值
def getX(index): # 获取当前index在第几行
if 0 <= index <= 2:
return 0
if 3 <= index <= 5:
return 1
if 6 <= index <= 8:
return 2
def getY(index): # 获取当前index在第几列
if index % 3 == 0:
return 0
elif (index + 1) % 3 == 0:
return 2
else:
return 1
currentIndex = current.index(value) # 获取当前下标
currentX = getX(currentIndex)
currentY = getY(currentIndex)
endIndex = end.index(value) # 获取终止下标
endX = getX(endIndex)
endY = getY(endIndex)
length = abs(endX - currentX) + abs(endY - currentY)
return length
def countTotalLength(current, end):
# 根据current和end计算current每个棋子与目标位置之间的距离和【除0】
count = 0
for item in current:
if item != 0:
count = count + computedLengthtoEndArray(item, current, end)
return count
def printArray(array): # 控制打印格式
print(str(array[0:3]) + '\n' + str(array[3:6]) + '\n' + str(array[6:9]) + '\n')
def getReverseNum(array): # 得到指定数组的逆序数 包括0
count = 0
for i in range(len(array)):
for j in range(i + 1, len(array)):
if array[i] > array[j]:
count = count + 1
return count
openList = [] # open表 存放实例对象
closedList = [] # closed表
endArray = [1, 2, 3, 8, 0, 4, 7, 6, 5] # 最终状态
countDn = 0 # 执行的次数
initObject = statusObject() # 初始化状态
# initObject.array = [2, 8, 3, 1, 6, 4, 7, 0, 5]
initObject.array = [2, 8, 3, 1, 6, 4, 7, 0, 5]
# initObject.array = [2, 1, 6, 4, 0, 8, 7, 5, 3]
initObject.Fn = countDn + countNotInPosition(initObject.array, endArray)
# initObject.Fn = countDn + countTotalLength(initObject.array, endArray)
openList.append(initObject)
zeroIndex = openList[0].array.index(0)
# 先做逆序奇偶性判断 0位置不算
initRev = getReverseNum(initObject.array) - zeroIndex # 起始序列的逆序数
print("起始序列逆序数", initRev)
endRev = getReverseNum(endArray) - endArray.index(0) # 终止序列的逆序数
print("终止序列逆序数", endRev)
res = countTotalLength(initObject.array, endArray)
# print("距离之和为", res)
# @SCY164759920
# 若两逆序数的奇偶性不同,则该情况无解
if((initRev%2==0 and endRev%2==0) or (initRev%2!=0 and endRev%2!=0)):
finalFlag = 0
while(1):
# 判断是否为end状态
if(operator.eq(openList[0].array,endArray)):
# 更新表,并退出
deep = openList[0].Dn
finalFlag = finalFlag +1
closedList.append(openList[0])
endList = []
del openList[0]
if(finalFlag == 1):
father = closedList[-1].Father
endList.append(endArray)
print("最终状态为:")
printArray(endArray)
while(father.Dn >=1):
endList.append(father.array)
father = father.Father
endList.append(initObject.array)
print("【变换成功,共需要" + str(deep) +"次变换】")
for item in reversed(endList):
printArray(item)
sys.exit()
else:
countDn = countDn + 1
# 找到选中的状态0下标
zeroIndex = openList[0].array.index(0)
# 获得该位置可select的operation
operation = selectOperation(zeroIndex, openList[0].cameFrom)
# print("0的下标", zeroIndex)
# print("cameFrom的值", openList[0].cameFrom)
# print("可进行的操作",operation)
# # print("深度",openList[0].Dn)
# print("选中的数组:")
# printArray(openList[0].array)
# 根据可选择的操作算出对应的序列
tempStatusList = []
for opeNum in operation:
# 根据操作码返回改变后的数组
copyArray = openList[0].array.copy()
newArray = setArrayByOperation(zeroIndex, copyArray, opeNum)
newStatusObj = statusObject() # 构造新对象插入open表
newStatusObj.array = newArray
newStatusObj.Dn = openList[0].Dn + 1 # 更新dn 再计算fn
newFn = newStatusObj.Dn + countNotInPosition(newArray, endArray)
# newFn = newStatusObj.Dn + countTotalLength(newArray, endArray)
newStatusObj.Fn = newFn
newStatusObj.cameFrom = opeNum
newStatusObj.Father = openList[0]
tempStatusList.append(newStatusObj)
# 将操作后的tempStatusList按Fn的大小排序
tempStatusList.sort(key=lambda t: t.Fn)
# 更新closed表
closedList.append(openList[0])
# 更新open表
del openList[0]
for item in tempStatusList:
openList.append(item)
# 根据Fn将open表进行排序
openList.sort(key=lambda t: t.Fn)
# print("第"+str(countDn) +"次的结果:")
# print("open表")
# for item in openList:
# print("Fn" + str(item.Fn))
# print("操作" + str(item.cameFrom))
# print("深度"+str(item.Dn))
# printArray(item.array)
# @SCY164759920
# print("closed表")
# for item2 in closedList:
# print("Fn" + str(item2.Fn))
# print("操作" + str(item2.cameFrom))
# print("深度" + str(item2.Dn))
# printArray(item2.array)
# print("==================分割线======================")
else:
print("该种情况无解")
2022.10.28 13:32 update:
After testing, it is found that the output of the source code will be BUG in some cases. It has been modified, the original data structure has been modified, and the "Father" attribute has been added to each node to store the father node of each node. After modification, it has been tested and can be output normally. If readers read this article after the update time, they can ignore it directly.
renew:
The heuristic function of the original program only provides the method of w(n), now update the implementation of p(n):
[p(n) is: the sum of the distances between each piece of node n and the target position]
Modification method:
Replace the two calculation functions of Fn in the source program and add two calculation functions
first place
【原】:initObject.Fn = countDn + countNotInPosition(initObject.array, endArray)
【替换】:initObject.Fn = countDn + countTotalLength(initObject.array, endArray)
Second place:
【原】:newFn = newStatusObj.Dn + countNotInPosition(newArray, endArray)
[Replacement]: newFn = newStatusObj.Dn + countTotalLength(newArray, endArray)
Add two calculation functions:
def computedLengthtoEndArray(value, current, end): # 两元素的下标之差并去绝对值
def getX(index): # 获取当前index在第几行
if 0 <= index <= 2:
return 0
if 3 <= index <= 5:
return 1
if 6 <= index <= 8:
return 2
def getY(index): # 获取当前index在第几列
if index % 3 == 0:
return 0
elif (index + 1) % 3 == 0:
return 2
else:
return 1
currentIndex = current.index(value) # 获取当前下标
currentX = getX(currentIndex)
currentY = getY(currentIndex)
endIndex = end.index(value) # 获取终止下标
endX = getX(endIndex)
endY = getY(endIndex)
length = abs(endX - currentX) + abs(endY - currentY)
return length
def countTotalLength(current, end):
# 根据current和end计算current每个棋子与目标位置之间的距离和【除0】
count = 0
for item in current:
if item != 0:
count = count + computedLengthtoEndArray(item, current, end)
return count
Run the heuristic function separately to take w(n) and p(n) and find that in the example conversion selected this time:
When taking p(n), the conversion process needs 5 steps in total
When w(n) is taken, the conversion process needs 5 steps in total