1. Descripción del problema
El problema de los ocho dígitos también se conoce como el problema de los nueve palacios. En un tablero de ajedrez de 3×3, hay ocho piezas de ajedrez, cada una marcada con un número del 1 al 8, y los números marcados en diferentes piezas son diferentes. También hay un espacio en el tablero (representado por el número 0), y las piezas adyacentes al espacio se pueden mover al espacio. El problema a resolver es: dado un estado inicial y un estado objetivo, encuentre un paso en movimiento con el menor número de piezas en movimiento desde el estado inicial hasta el estado objetivo.
Este problema se puede resolver con el algoritmo de búsqueda heurística A*.
La función de evaluación del algoritmo A* es la siguiente:
Entre ellos, la función heurística puede elegir w(n) y p(n).Este artículo usa w(n) como ejemplo para escribir un programa
2. Implementación del algoritmo [parte teórica]
Este artículo toma como ejemplo de análisis la siguiente situación:
1. Abstraer el problema
① Selección de operador
Si nos enfocamos en el número, el operador de operación correspondiente es el movimiento del número, se puede ver que hay 4 (dirección) * 8 (número de códigos) = 32 tipos de operadores, y el diseño es más complicado.
Si te enfocas en el espacio, el operador de operación correspondiente es el movimiento del espacio. En la situación más ideal [es decir, cuando el espacio está en el medio del tablero de ajedrez], hay como máximo 4 tipos de operadores [mover hacia arriba , mover hacia abajo y mover hacia la izquierda, izquierda, derecha], el diseño es relativamente simple.
En resumen, este programa opta por centrarse en los espacios y utiliza los cuatro operadores de arriba, abajo, izquierda y derecha para la programación.
② Abstracción del proceso de movimiento digital
La distribución de números en el tablero de ajedrez 3*3 se puede abstraer en una matriz unidimensional, de modo que cada movimiento del espacio sea equivalente a intercambiar las posiciones de dos elementos en la matriz unidimensional , como se muestra en la siguiente figura:
Hasta ahora, el problema del movimiento digital se ha transformado en el problema de encontrar los subíndices de los dos elementos a intercambiar en la matriz e intercambiarlos.
2. Proceso de ejecución real
① Árbol de búsqueda
Desde el árbol de búsqueda en la figura anterior, podemos ver que:
Cualquier nodo en el árbol debe contener la siguiente información:
La profundidad del nodo en el árbol.
El valor de la función de evaluación f(n) del nodo
La secuencia digital del nodo [abstraído como una matriz unidimensional]
② mesa abierta y mesa cerrada
Se puede ver en la tabla anterior:
La tabla abierta almacena los nodos ordenados por el valor de la función de evaluación , y la tabla cerrada almacena el primer nodo extraído de la tabla abierta cada vez que pasa por el ciclo, hasta que la secuencia numérica del nodo extraído sea igual al número final. secuencia, y el algoritmo termina.
3. Situación irresoluble
En primer lugar, es claro que el problema de ocho dígitos no tiene solución. Al escribir un programa, primero es necesario juzgar si la transformación entre la secuencia inicial y la secuencia objetivo tiene solución. Si las dos secuencias sin solución son ejecutado, el algoritmo quedará atrapado en un bucle sin fin
El juicio de si los dos estados tienen soluciones está determinado por la paridad de los números de inversión de las dos secuencias.Si los números de inversión de las dos secuencias son pares o impares , la transformación de las dos secuencias tiene una solución, de lo contrario no hay sin solución.
¿Qué es un número ordinal inverso? ¿Cómo encontrar el número ordinal inverso? Lea este artículo, aquí no hay mucho que repetir el artículo ordinal inverso
4. Selección de operador
① Condiciones de contorno
② Evitar el bucle infinito
Se realizó la operación ARRIBA anterior, y la operación ABAJO debe deshabilitarse en la próxima iteración
Se realizó la operación IZQUIERDA anterior, la siguiente iteración debería deshabilitar la operación DERECHA,
Y viceversa, el propósito es evitar un bucle infinito
El ejemplo específico es el siguiente:
En resumen, cuando la selección de operadores no considera las restricciones en ② , el resultado de la selección es muy fijo, por ejemplo, solo se pueden seleccionar ABAJO y DERECHA para la posición 0, y solo IZQUIERDA, ARRIBA y ABAJO para la posición 0. posición 5. En el programa real, el operador que se puede seleccionar esta vez se puede obtener de acuerdo con la posición del elemento en la matriz + restricciones .
5. Diseño de estructura de datos
Después del análisis anterior, podemos ver que para implementar el algoritmo, la clave es diseñar la estructura de datos de cada nodo en el árbol de búsqueda, la estructura de este diseño es la siguiente:
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. Implementación del algoritmo [parte del código]
1. Diagrama de flujo:
2. Código fuente del programa
El programa usa el paquete numpy, instálelo usted mismo antes de ejecutarlo
Además, se utilizaron muchas declaraciones de impresión para ver los resultados durante el proceso de depuración, que se ha comentado, elimínelo usted mismo si no lo necesita.
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 actualización:
Después de la prueba, se encuentra que la salida del código fuente será BUG en algunos casos. Se modificó, se modificó la estructura de datos original y se agregó el atributo "Padre" a cada nodo para almacenar el nodo padre. de cada nodo. Después de la modificación, se ha probado y se puede generar normalmente. Si los lectores leen este artículo después del tiempo de actualización, pueden ignorarlo directamente.
renovar:
La función heurística del programa original solo proporciona el método de w(n), ahora actualice la implementación de p(n):
[p(n) es: la suma de las distancias entre cada parte del nodo n y la posición de destino]
Método de modificación:
Reemplace las dos funciones de cálculo de Fn en el programa fuente y agregue dos funciones de cálculo
primer lugar
【Objeto】:initObject.Fn = countDn + countNotInPosition(initObject.array, endArray)
【Resumen】:initObject.Fn = countDn + countTotalLength(initObject.array, endArray)
Segundo lugar:
【原】:newFn = newStatusObj.Dn + countNotInPosition(newArray, endArray)
[Reemplazo]: newFn = newStatusObj.Dn + countTotalLength(newArray, endArray)
Agregue dos funciones de cálculo:
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
Ejecute la función heurística por separado para tomar w(n) y p(n) y descubra que en el ejemplo de conversión seleccionado esta vez:
Al tomar p(n), el proceso de conversión necesita 5 pasos en total
Cuando se toma w(n), el proceso de conversión necesita 5 pasos en total