人工知能 - 8 つのデジタル問題を解決するヒューリスティック検索アルゴリズム* Python の実装

1. 問題の説明

        8 桁の問題は、9 つ​​の宮殿の問題とも呼ばれます。3×3 のチェス盤には 8 つのチェスの駒があり、それぞれに 1 から 8 までの数字が記されており、各駒に記されている数字は異なります。ボード上にもスペースがあり (数字の 0 で表されます)、スペースに隣接するピースをスペースに移動できます。解決すべき問題は、初期状態と目標状態が与えられたとき、初期状態から目標状態への移動駒の数が最も少ない移動ステップを見つけることです。

この問題は、A* ヒューリスティック検索アルゴリズムで解決できます。

A* アルゴリズムの評価関数は次のとおりです。 

        ヒューリスティック関数はその中から w(n) と p(n) を選ぶことができます. この記事では w(n) を例にプログラムを書きます.


2. アルゴリズムの実装【理論部分】

この記事では、分析の例として次の状況を取り上げます。

         1. 問題を抽象化する

                 ①. オペレーターの選定

                数字に着目すると、対応する演算演算子は数字の動きであり、4(方向)×8(コード数)=32種類の演算子があり、設計がより複雑であることがわかります。

                空間に注目すると、対応する操作演算子は空間の移動であり、最も理想的な状況 (つまり、空間がチェス盤の中央にあるとき) では、多くても 4 種類の演算子 [上に移動する] が存在します。 、下に移動、左に移動、右に移動]、デザインは比較的シンプルです。

                要約すると、このプログラムはスペースに焦点を当てることを選択し、プログラミングに上、下、左、右の 4 つの演算子を使用します。

               ②. デジタル移動プロセスの抽象化

次の図に示すように、                3*3 チェス盤上の数字の分布は1 次元配列に抽象化できるため、空間の各移動は1 次元配列内の2 つの要素の位置を交換することと同じです。

     

 これまでのところ、デジタル移動の問題は、配列で交換する 2 つの要素の添字を見つけて交換する問題に変換されました。

                2. 実際の執行プロセス

                        ①. 検索ツリー

上の図の検索ツリーから、次のことがわかります。

        ツリーのノードには、次の情報が含まれている必要があります。

                ツリー内のノードの深さ

                ノードの評価関数値 f(n)

                ノードのデジタル シーケンス [1 次元配列として抽象化]

                        ②.オープンテーブルとクローズテーブル

上記の表から次のことがわかります。

開いたテーブルには評価関数 の値        で​​ソートされたノードが格納され、閉じたテーブルにはループのたびに開いたテーブルから最初に取り出されたノードが格納され、取り出されたノードの数列が最終的な番号と等しくなるまで続きます。シーケンス、およびアルゴリズムが終了します。 

          3. 解決不可能な状況

まず, 8 桁問題には解がないことは明らかである. プログラムを書くときは, 最初の数列と目的の数列の間の変換が解を持つかどうかを判断する必要がある. 解のない 2 つの数列が実行すると、アルゴリズムは無限ループに陥ります

        2 つの状態が解を持つかどうかの判断は2 つの数列の反転数のパリティによって決定されます. 2 つの数列の反転数が両方とも奇数または偶数の場合, 2 つの数列の変換は解を持ちます. そうでない場合は解決策はありません。

        逆序数とは?逆序数を見つける方法は? この記事を読んでください、ここでは逆序数の記事を繰り返すことはあまりありません  

            4. オペレーターの選定

                       ①. 境界条件                   

                         ②. 無限ループ防止

前回の UP 操作が実行されました。DOWN 操作は次の繰り返しで無効にする必要があります

前の LEFT 操作が実行されたので、次の反復では RIGHT 操作を無効にする必要があります。

逆もまた同様で、目的は無限ループを回避することです

具体例は次のとおりです。        

                つまり、②の制約を考慮しないで演算子を選択すると、選択結果が非常に固定的になり、例えば位置0はDOWNとRIGHTしか選択できず、位置0はLEFTとUPとDOWNしか選択できません。ポジション5。実際のプログラムでは、今回選択できる演算子は、配列内の要素の位置 + 制約によって取得できます。

        5. データ構造設計

        上記の分析の後、アルゴリズムを実装するには、検索ツリーの各ノードのデータ構造を設計することが重要であることがわかります.この設計の構造は次のとおりです。

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.アルゴリズムの実装【コード部】

         1. フローチャート:

                 2. プログラムのソースコード

プログラムは numpy パッケージを使用します。実行する前に自分でインストールしてください

また、デバッグ処理中に結果を表示するために大量の print ステートメントが使用されました。これはコメントされています。不要な場合は自分で削除してください。

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更新: 

テストの結果、ソースコードの出力がバグになる場合があることがわかりました.これは変更され、元のデータ構造が変更され、各ノードに「父」属性        が追加され、父ノードが格納されました.各ノードの。修正後は動作確認済みで正常に出力できますので、更新時刻以降にこの記事を読んだ読者はそのまま無視して構いません。

更新:

         元のプログラムのヒューリスティック関数は w(n) のメソッドのみを提供し、現在は p(n) の実装を更新しています。

        [p(n) は: ノード n の各ピースとターゲット位置の間の距離の合計]

修正方法:

        ソースプログラムの Fn の 2 つの計算関数を置き換え、2 つの計算関数を追加します。

最初の場所

 【原】:initObject.Fn = countDn + countNotInPosition(initObject.array, endArray)

【置換】:initObject.Fn = countDn + countTotalLength(initObject.array, endArray)

二位:

【原】:newFn = newStatusObj.Dn + countNotInPosition(newArray, endArray)

[置き換え]: newFn = newStatusObj.Dn + countTotalLength(newArray, endArray)

2 つの計算関数を追加します。

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

ヒューリスティック関数を個別に実行してw(n)p(n)を取得し、今回選択した変換例で次のことを見つけます。

        p(n) を取得する場合、変換プロセスには合計 5 つのステップが必要です

        w(n)を取ると変換処理は全部で5ステップ必要

おすすめ

転載: blog.csdn.net/SCY164759920/article/details/127164952