启发式搜索--八数码问题

八数码问题

问题描述:
八数码问题也称为九宫问题。在3×3的棋盘上摆有八个棋子,每个棋子上标有1至8的某一数字,不同棋子上标的数字不相同。棋盘上还有一个空格(数字表为0),与空格相邻的棋子可以移到空格中。
需要解决的问题:给出一个初始状态和一个目标状态,求出从初始状态转变成目标状态的移动棋子的最少步数。

启发式搜索

  • 原理:启发式搜索就是在状态空间中的搜索对每一个搜索的位置进行评估,得到最好的位置,再从这个位置进行搜索直到目标。这样可以省略大量无谓的搜索路径,提高了效率。在启发式搜索中,对位置的估价是十分重要的。采用了不同的估价可以有不同的效果。

  • 估价函数:
    f ∗ ( n ) = h ∗ ( n ) + g ∗ ( n ) f^*(n) = h^*(n) + g^*(n) f(n)=h(n)+g(n)
    其中,在任意节点 n n n上定义, g ∗ ( n ) g^*(n) g(n)为从初始节点 S S S到节点 n n n的代价; h ∗ ( n ) h^*(n) h(n)为从节点 n n n到目标节点 E E E的代价;则 f ∗ ( n ) f^*(n) f(n)定义为从初始节点S经过节点 n n n到达目标节点 E E E的最佳路径代价。
    定义 f ∗ ( n ) f^*(n) f(n) 的估计函数 f ( n ) f(n) f(n) f ( n ) = h ( n ) + g ( n ) f(n) = h(n) + g(n) f(n)=h(n)+g(n)
    其中, g ( n ) g(n) g(n) g ∗ ( n ) g^*(n) g(n)的估计函数, f ( n ) f(n) f(n) f ∗ ( n ) f^*(n) f(n)的估计函数。这样定义的估价函数为 A 算 A算 A

  • 启发式 A A A算法:

    1. 将初始节S点放入OPEN表,计算其 f ( S ) f(S) f(S)
    2. OPEN表为空搜索失败,退出。
    3. 从OPEN表中选择一个 f f f值最小的节点 i i i。如果该节点为目标节点,则搜索成功退出。
    4. 把节点 i i i从OPEN表中移除,放入CLOSE的已扩展节点表中。
    5. 扩展节点 i i i,生成所有后继节点。对于每个后继节点 j j j(进行父子节点连接):
      计算 f ( j ) f(j) f(j);如果 j j j即不在OPEN也不在CLOSE中,这把节点 j j j放入OPEN表中;如果 j j j已在OPEN或CLOSE表中,则比较新旧节点的 f f f值,如果新节点的值更小,则:
      1. 以新节点替代旧节点
      2. 如果旧节点在CLOSE表中,则将其移回OPEN表中
    6. 退回第2步进行循环
  • A A A算法中,当所有节点对于 n n n 0 < = h ( n ) < = h ∗ ( n ) 0<=h(n)<=h^*(n) 0<=h(n)<=h(n)时(保证了算法的可采纳性), A A A算法成为 A ∗ A^* A算法。

使用A*算法解决八数码问题

代码实现时,笔者用State类表示搜索状态,数位digits用长度为9的一维列表表示。 g ( n ) g(n) g(n)为运行深度, h ( n ) h(n) h(n)为可变函数(可以类外更改)。 A ∗ A^* A算法按照算法描述实现。(代码可进一步优化,例如找最小 f f f值时,笔者偷懒 采用扫描查找,可以通过后续扩展节点插入OPEN表时采用二分插入排序的方法进行优化)
代码中提供了两个可使用的 h ( n ) h(n) h(n)函数(默认恒为0,原算法即为BFS搜索),其中第一个函数的代价定义为每一位数码与其目标位置之间的距离的总和;第二个函数的代价定义为数码错放的数量。

八数码的可解性问题,详细证明自行搜索,这里直接提供结论:把数字排列组成数列,如果初始状态和目标状态的数列的逆序数的奇偶性相同,则可由初始状态移动至目标状态,反之不可。

多说无益, 上代码

代码写的垃圾,勿喷

# 状态类
class State:
    # h(n) 将付出的代价
    h = lambda self: 0

    def __init__(self, digits, depth, pa):
        # 数位
        self.digits = digits
        # 父状态
        self.pa = pa
        # 深度 g(n)已付出的代价
        self.depth = depth
        # 估价函数
        self.f = self.__f()

    # 交换zero位和next位的数
    def get_digits(self, zero, next):
        digits = list(self.digits)
        digits[zero] = digits[next]
        digits[next] = 0
        return digits

    # 获取可移动的下一个digits
    def next_digits(self):
        zero = [i for i, v in enumerate(self.digits) if v == 0][0]
        if int(zero / 3) != 0:
            yield self.get_digits(zero, zero-3)
        if int(zero / 3) != 2:
            yield self.get_digits(zero, zero+3)
        if zero % 3 != 0:
            yield self.get_digits(zero, zero-1)
        if zero % 3 != 2:
            yield self.get_digits(zero, zero+1)

    # 获取下一个可转换状态(父状态不在内)
    def next_state(self):
        for d in self.next_digits():
            if not self.pa or d != self.pa.digits:
                yield State(d, self.depth + 1, self)

    # 计算估价函数f(n) = g(n) + h(n)
    def __f(self):
        return self.depth + State.h(self) * 2

    # 更换h(x)
    @staticmethod
    def change_h(h):
        State.h = h
        pass

    def __eq__(self, other):
        return self.digits == other.digits

    def __str__(self):
        res = 'depth = %d, f(x) = %d\n' % (self.depth, self.f)
        for i in range(3):
            res += str(self.digits[i*3: (i+1)*3]) + '\n'
        return res


class EightDigits:
    def __init__(self, s, e = [1, 2, 3, 8, 0, 4, 7, 6, 5]):
        # 初始状态
        self.s = State(s, 0, None)
        # 目标状态
        self.e = e
        self.open = []
        self.close = []
        self.open.append(self.s)

    # 判断奇偶排列
    def isOdd(self, digits):
        num = 0
        for i in range(9):
            if digits[i] == 0:
                continue
            for j in range(i + 1, 9):
                if digits[j] == 0:
                    continue
                if digits[i] > digits[j]:
                    num += 1
        return num % 2 == 1

    # A*算法
    def A(self):
    	# 判断可不可解
        if self.isOdd(self.s.digits) != self.isOdd(self.e):
            return None
        num = 0
        while self.open:
            front = min(self.open, key=lambda x: x.f)
            self.open.remove(front)
            num += 1
            print(front)
            if front.digits == self.e:
            	# 搜索次数
                print("-" * 10 + str(num) + "-" * 10 + '\n')
                return front
            self.close.append(front)
            for next in front.next_state():
                if next in self.open:
                    for i, o in enumerate(self.open):
                        if next == o:
                            if next.f < o.f:
                                self.open[i] = next
                        break
                elif next in self.close:
                    for c in self.close:
                        if next == c:
                            if next.f < c.f:
                                self.close.remove(c)
                                self.open.append(next)
                        break
                else:
                    self.open.append(next)
        return None

    @staticmethod
    def show(state):
        if state.pa:
            EightDigits.show(state.pa)
        print(state)


# h函数1 代价为所有数恢复原有状态所移动距离的代价总和
def p(s):
    e = [1, 2, 3, 8, 0, 4, 7, 6, 5]
    digits = s.digits
    res = 0
    for i in range(9):
        for j, v in enumerate(digits):
            if v == e[i]:
                t = abs(i - j)
                res += int(t / 3) + t % 3
                break
    return res


# h函数2 代价为不在正确位置的数的数量
def w(s):
    e = [1, 2, 3, 8, 0, 4, 7, 6, 5]
    digits = s.digits
    return len([i for i in range(9) if e[i] != digits[i]])


if __name__ == '__main__':
    State.change_h(p) # 采用h函数1
    eight_digits = EightDigits([2, 1, 3, 0, 8, 7, 4, 6, 5])
    state = eight_digits.A()
    if not state:
        print('初始状态无法移动至目标状态')
    else:
        EightDigits.show(state)

你可能感兴趣的:(demo)