python解决八数码问题_A*算法实践——八数码问题

本文通过Python展示了如何使用A*算法解决经典的八数码问题。详细介绍了算法的实现过程,包括设置初始和目标状态,计算曼哈顿距离,定义移动函数,以及A*算法的主要步骤。通过搜索树找到从初始状态到目标状态的最优路径。
摘要由CSDN通过智能技术生成

源代码在最后。

接下来会用Python来实现A*算法求解八数码问题。

八数码就是在3*3的棋盘中有8个数码(数字块)和一个空格,只有与空格相邻的数码能移动到空格位置。八数码

目的就是从初始状态通过移动数码到达指定状态。那么首先就设置一个初始状态吧,以免还要输入。目标状态就是上面那张图的状态。初始状态

代码

代价 = 从初始状态到当前状态的代价 + 从当前状态到目标状态的估计代价

那么,从初始状态到当前状态的代价就用移动的步数表示,从当前状态到目标状态的估计代价就用所有数码与它们的最终位置的曼哈顿距离表示。比如说数码2当前位于(2,2),而它的目的位置是(0,1),所以曼哈顿距离就是abs(0-2)+abs(1-2)=3。

好了 ,接下来建立代码的初始状态和目标状态:图1

goal_dic用于把目标状态的数码对应到位置,以便计算曼哈顿距离。下图就是曼哈顿距离的计算:图2

这两个函数是用来输出状态和复制状态的:图3

获取空格位置及四个方向上的移动:图4

获取指定状态下可以调用哪些移动函数,Start是为了统一节点信息的,因为每个节点都有一个操作属性,表示从其父节点到达此节点要用何种操作。当然,最重要的是输出操作名。图5

搜索的结果就是建立一棵搜索树,如果成功到达目标状态,则返回目标节点,然后遍历输出就可以知道如何操作可以到达目标节点了。图6

已经扩展的节点保存在一个队列中,最好是使用优先队列。我是要做作业才刚学的Python,所以就用列表表示队列了,然后用一个函数找代价最小的元素。那个toInt是用来将状态转换为整数值然后作为已经访问过了的表的键的,原因嘛,就是Python不允许用列表作为哈希表的键(我手动哈希还不行吗^_^)。:图7

接下来是主角登场,A星算法的主循环:图8

还有类似主函数的调用。reverse用于反转路径,因为搜索返回的是目标节点,链是反的:图9

结果图10

图11

图12

图13

源码

# 初始状态

init_state = [

[1, 8, 7],

[3, 0, 5],

[4, 6, 2]

]

# 目标状态

goal_state = [

[1, 2, 3],

[4, 5, 6],

[7, 8, 0]

]

# 目标状态 值-位置表

goal_dic = {

1:(0,0), 2:(0,1), 3:(0,2),

4:(1,0), 5:(1,1), 6:(1,2),

7:(2,0), 8:(2,1), 0:(2,2)

}

# 输出状态

def PrintState(state):

for i in state: print(i)

# 复制状态

def CopyState(state):

s = []

for i in state: s.append(i[:])

return s

# 获取空格的位置

def GetSpace(state):

for y in range(len(state)):

for x in range(len(state[y])):

if state[y][x] == 0: return y, x

# 获取空格上移后的状态,不改变原状态

def MoveUp(state):

s = CopyState(state)

y, x = GetSpace(s)

s[y][x], s[y - 1][x] = s[y - 1][x], s[y][x]

return s

# 获取空格下移后的状态,不改变原状态

def MoveDown(state):

s = CopyState(state)

y, x = GetSpace(s)

s[y][x], s[y + 1][x] = s[y + 1][x], s[y][x]

return s

# 获取空格左移后的状态,不改变原状态

def MoveLeft(state):

s = CopyState(state)

y, x = GetSpace(s)

s[y][x], s[y][x - 1] = s[y][x - 1], s[y][x]

return s

# 获取空格右移后的状态,不改变原状态

def MoveRight(state):

s = CopyState(state)

y, x = GetSpace(s)

s[y][x], s[y][x + 1] = s[y][x + 1], s[y][x]

return s

# 获取两个状态之间的启发距离

def GetDistance(src, dest):

dic, d = goal_dic, 0

for i in range(len(src)):

for j in range(len(src[i])):

pos = dic[src[i][j]]

y, x= pos[0], pos[1]

d += abs(y - i) + abs(x - j)

return d

# 获取指定状态下的操作

def GetActions(state):

acts = []

y, x = GetSpace(state)

if x > 0:acts.append(MoveLeft)

if y > 0:acts.append(MoveUp)

if x < len(state[0]) - 1:acts.append(MoveRight)

if y < len(state[0]) - 1: acts.append(MoveDown)

return acts

# 用于统一操作序列的函数

def Start(state):

return

# 边缘队列中的节点类

class Node:

state = None   # 状态

value = -1     # 启发值

step = 0       # 初始状态到当前状态的距离(步数)

action = Start  # 到达此节点所进行的操作

parent = None,  # 父节点

# 用状态和步数构造节点对象

def __init__(self, state, step, action, parent):

self.state = state

self.step = step

self.action = action

self.parent = parent

# 计算估计距离

self.value = GetDistance(state, goal_state) + step

# 获取拥有最小启发值的元素索引

def GetMinIndex(queue):

index = 0

for i in range(len(queue)):

node = queue[i]

if node.value < queue[index].value:

index = i

return index

# 将状态转换为整数

def toInt(state):

value = 0

for i in state:

for j in i:

value = value * 10 + j

return value

# A*算法寻找初始状态到目标状态的路径

def AStar(init, goal):

# 边缘队列初始已有源状态节点

queue = [Node(init, 0, Start, None)]

visit = {}  # 访问过的状态表

count = 0   # 循环次数

# 队列没有元素则查找失败

while queue:

# 获取拥有最小估计距离的节点索引

index = GetMinIndex(queue)

node = queue[index]

visit[toInt(node.state)] = True

count += 1

if node.state == goal:

return node, count

del queue[index]

# 扩展当前节点

for act in GetActions(node.state):

# 获取此操作下到达的状态节点并将其加入边缘队列中

near = Node(act(node.state), node.step + 1, act, node)

if toInt(near.state) not in visit:

queue.append(near)

return None, count

# 将链表倒序,返回链头和链尾

def reverse(node):

if node.parent == None:

return node, node

head, rear = reverse(node.parent)

rear.parent, node.parent = node, None

return head, node

node, count = AStar(init_state, goal_state)

if node == None:

print("无法从初始状态到达目标状态!")

else:

print("搜索成功,循环次数:", count)

node, rear = reverse(node)

count = 0

while node:

# 启发值包括从起点到此节点的距离

print("第", count + 1, "步:", node.action.__name__,

"启发值为:", count, "+", node.value - count)

PrintState(node.state)

node = node.parent

count += 1

  • 1
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
A*算法求解八数码问题 1、A*算法基本思想: 1)建立一个队列,计算初始结点的估价函数f,并将初始结点入队,设置队列头和尾指针。 2)取出队列头(队列头指针所指)的结点,如果该结点是目标结点,则输出路径,程序结束。否则对结点进行扩展。 3)检查扩展出的新结点是否与队列中的结点重复,若与不能再扩展的结点重复(位于队列头指针之前),则将它抛弃;若新结点与待扩展的结点重复(位于队列头指针之后),则比较两个结点的估价函数中g的大小,保留较小g值的结点。跳至第五步。 4)如果扩展出的新结点与队列中的结点不重复,则按照它的估价函数f大小将它插入队列中的头结点后待扩展结点的适当位置,使它们按从小到大的顺序排列,最后更新队列尾指针。 5)如果队列头的结点还可以扩展,直接返回第二步。否则将队列头指针指向下一结点,再返回第二步。 2、程序运行基本环境: 源程序所使用编程语言:C# 编译环境:VS2010,.net framework 4.0 运行环境:.net framework 4.0 3、程序运行界面 可使用程序中的test来随机生成源状态与目标状态 此停顿过程中按Enter即可使程序开始运行W(n)部分; 此停顿部分按Enter后程序退出; 4、无解问题运行情况 这里源程序中是先计算源状态与目标状态的逆序对的奇偶性是否一致来判断是否有解的。下面是无解时的运行画面: 输入无解的一组源状态到目标状态,例如: 1 2 3 4 5 6 7 8 0 1 2 3 4 5 6 8 7 0 运行画面如下: 5、性能比较 对于任一给定可解初始状态,状态空间有9!/2=181440个状态;当采用不在位棋子数作为启发函数时,深度超过20时,算法求解速度较慢; 其中启发函数P(n)与W(n)的含义如下: P(n): 任意节点与目标结点之间的距离; W(n): 不在位的将牌数; 源状态 目标状态 P(n) 生成节点数 W(n) 生成节点数 P(n) 扩展节点数 W(n) 扩展节点数 2 8 3 1 6 4 7 0 5 1 2 3 8 0 4 7 6 5 11 13 5 6 1 2 3 8 0 4 7 6 5 0 1 3 8 2 4 7 6 5 6 6 2 2 4 8 2 5 1 6 7 0 3 7 4 2 8 5 6 1 3 0 41 79 22 46 6 2 5 8 7 0 3 1 4 0 3 6 7 1 8 4 5 2 359 10530 220 6769 7 6 3 1 0 4 8 5 2 2 8 7 1 3 4 6 5 0 486 8138 312 5295 下图是解决随机生成的100中状态中,P(n)生成函数的生成节点与扩展节点统计图: 由上图可知,P(n)作为启发函数,平均生成节点数大约在1000左右,平均扩展节点数大约在600左右; 下图是解决随机生成的100中状态中,W(n)生成函数的生成节点与扩展节点统计图: 由上图可知,W (n)作为启发函数,平均生成节点数大约在15000左右,是P(n)作为启发函数时的平均生成节点的15倍;W (n)作为启发函数,平均扩展节点数大约在10000左右,是P(n)作为启发函数时的平均扩展节点的15倍; 下图是解决随机生成的100中状态中,两个生成函数的生成节点与扩展节点统计图: 由上述图表可以看到,将P(n)作为启发函数比将W(n)作为启发函数时,生成节点数与扩展节点数更稳定,相比较来说,采用P(n)作为启发函数的性能比采用W(n)作为启发函数的性能好。 6、源代码说明 1)AStar-EightDigital-Statistics文件夹:用来随机生成100个状态,并对这100个状态分别用P(n)与W(n)分别作为启发函数算出生成节点以及扩展节点,以供生成图表使用;运行界面如下: 2)Test文件夹:将0-8这9个数字随机排序,用来随机生成源状态以及目标状态的;运行界面如下: 3)AStar-EightDigital文件夹:输入源状态和目标状态,程序搜索出P(n)与W(n)分别作为启发函数时的生成节点数以及扩展节点数,并给出从源状态到目标状态的移动步骤;运行界面如下: 提高了运行速度的几处编码思想: 1、 在维护open以及close列表的同时,也维护一个类型为hashtable的open以及close列表,主要用来提高判断当前节点是否在open列表以及close列表中出现时的性能; 2、 对于每个状态,按照从左到右,从上到下,依次将数字拼接起来,形成一个唯一标识identify,通过该标识,可以直接判断两个状态是否是同一个状态,而不需要循环判断每个位置上的数字是否相等 3、 在生成每个状态的唯一标识identify时,同时计算了该状态的空格所在位置,通过空格所在位置,可以直接判断能否进行上移、下移、左移、右移等动作; 4、 只计算初始节点的h值,其它生成的节点的h值是根据当前状态的h值、移动的操作等计算后得出的,规则如下: a) 采用W(n)这种方式,不在位置的将牌数,共有以下3中情况: i. 该数字原不在最终位置上,移动后,在其最终位置上 这种情况下,生成的子节点的h值= 父节点的h值-1 ii. 该数字原在最终位置上,移动后,不在其最终位置上 这种情况下,生成的子节点的h值= 父节点的h值 +1 iii. 该数字原不在最终位置上,移动后,还是不在其最终位置上 这种情况下,生成的子节点的h值= 父节点的h值 iv. 该数字原在最终位置上,移动后,还在其最终位置 这种情况不存在 b) 采用P(n)这种方式,节点与目标距离,可通过下面3步完成 i. 首先计算在原位置时,与目标位置的距离,命名为Distance1 ii. 移动后,计算当前位置与目标位置的距离,命名为Distance2 iii. 计算子节点的h值: 子节点的h值 = 父节点的h值- Distance1+ Distance2 5、 在任意状态中的每个数字和目标状态中同一数字的相对距离就有9*9种,可以先将这些相对距离算出来,用一个矩阵存储,这样只要知道两个状态中同一个数字的位置,就可查出它们的相对距离,也就是该数字的偏移距离;例如在一个状态中,数字8的位置是3,在另一状态中位置是7,那么从矩阵的3行7列可找到2,它就是8在两个状态中的偏移距离。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值