Artificial intelligence-A* heuristic search algorithm to solve eight digital problems Python implementation

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                   

                         ②. 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

Guess you like

Origin blog.csdn.net/SCY164759920/article/details/127164952