八数码问题


八数码实验报告

03122997
学校:上海大学学院:计算机学院

关键字:八数码、人工智能、A*算法、双向广搜、启发式函数

摘要:

九宫排字问题(又称八数码问题)是人工智能当中有名的难题之一。问题是在
3×3方格盘上,
放有八个数码,剩下第九个为空,每一空格其上下左右的数码可移至空格。问题给定初始位置和目
标位置,要求通过一系列的数码移动,将初始位置转化为目标位置。本文介绍用普通搜索方法、双
向广度搜索和启发式搜索如何缩短寻找路径的时间,以及各算法间的利与弊。

目录:

问题简介
问题分析
算法设计
程序实现
相关链接


问题简介:

所谓八数码问题是指这样一种游戏:将分别标有数字
1,2,3,…,
8的八块正方形数码牌任意
地放在一块
3×3的数码盘上。放牌时要求不能重叠。于是,在
3×3的数码盘上出现了一个空格。
现在要求按照每次只能将与空格相邻的数码牌与空格交换的原则,将任意摆放的数码盘逐步摆成某
种特殊的排列。如下图表示了一个具体的八数码问题求解。


问题分析:

首先,八数码问题包括一个初始状态
(START)和目标状态
(END),所谓解八数码问题就是在两
个状态间寻找一系列可过渡状态(
START>
STATE1>
STATE2>...
>
END)。这个状态是否存在就是我
们要解决的第一个问题:


Q1:每一个状态及每一次操作的表示方法?

有许多表示方法,比如一个
3*3的八数码盘可以压缩成一个
int值表示,但不适用于
15puzzle
或大于
8的
puzzle问题。如果对空间要求很高,应该还可以再压缩。本文采用一个
int表示的方法。

表示方法如下:由于
int的表示范围大于
1e9,所以我们取一个
int的低
9位,为了方便寻找空
格的位置,int的个位我们用来放空格的位置(19)
。而前
8位,按照行从上到下,列从左到右的顺
序依次记录对应位置上的数字。例如:



231584675,个位的
5表示空格在第
5位,前八位数按顺序记
录。坐标转换公式为:


num(压缩后的
int)
xy(求
x行
y列,1记起)1e(n)为
1乘
10的
n次
int
temp=(x1)*
3+y
if
temp>num%10thenreturn(num/1e(9temp+
1))%10
else
return(num/1e(9temp))%
10

为了方便本文介绍,取目标状态为:123456789即>



操作本文用
urdl分别表示空格的向上向右向下向左四个操作。比如,在简介中的图包
括两步操作
ld,可能与平时玩这类游戏的习惯不符合,但这是为了和
ACM例题相统一。

对应地,每种操作引起的状态变化如下:


r:num值++
l:num值u:
有点复杂
int
t0=
9num%
10+
1
int
t1=
num/1e(t0)

int
t2=
t1%1000
t1=
t1t2+
(t2
%100)
*
10+
t2
/100
t1*=
1e(t0)
return(t1+
((num%t0)
3))
d:return前面同
u操作,
return返回
(t1+
((num%t0)+
3))
Q2:判断是否存在中间状态使
START到达
END?

用组合数学的方法可以快速地进行判断,例如
SOJ
2061题
2360题中都是关于此类的问题。但
八数码的判断方法比他们简单多了。

本文用的方法是计算排列的逆序数值,以
231584675为例子,5表示的是空格,不计算,
那么求
23158467的逆序值为


0
+0
+2
(1<21<3)
+0+
0+
1(
4<5)+
1(6<8
)+1
(7<8)
=5

目标状态
123456789的逆序自然就是
0。

两个状态之间是否可达,可以通过计算两者的逆序值,若两者奇偶性相同则可达,不然两个状
态不可达。

简单证明一下:


l和
r操作,不会影响状态的逆序值,因为只会改变个位数(空格的位置)。


u和
d操作是使某个位置的数字右
/左移两位。由于数字序列的每一次移动会使逆序值奇偶性
改变,所以移动两次后奇偶性不变。

所以四个操作均不会影响序列的奇偶性。


Q3:如何寻找一系列的中间状态及遇到的问题?

要寻找这一系列中间状态的方法是搜索,但搜索很容易遇到时间和空间上的问题。以下就是搜
索的基本原理:





137246852状态可以衍生三个状态,假如选择

123746855,则又衍生三个状态,继续按某策略进
行选择,一直到衍生出的新状态为目标状态
END为止。

容易看出,这样的搜索类似于从树根开始向茎再向叶
搜索目标叶子一样的树型状。由于其规模的不断扩大,其
叶子也愈加茂密,最终的规模会大到无法控制。这样在空
间上会大大加大搜索难度,在时间上也要消耗许多。

在普通搜索中遇到以下问题:
搜索中易出现循环,即访问某一个状态后又来访问该
状态。
搜索路径不佳便无法得到较好的中间状态集(即中间
状态集的元素数量过大)。
搜索过程中访问了过多的无用状态,这些状态对最后
的结果无帮助。

以上三个问题中,a为致命问题,应该它可能导致程序死循环;
b和
c是非致命的,但若不处理
好可能导致性能急剧下降。


Q4:怎样避免重复访问一个状态?

最直接的方法是记录每一个状态访问否,然后再衍生状态时不衍生那些已经访问的状态了。思
想是,给每个状态标记一个
flag,若该状态
flag=true则不衍生,若为
false则衍生并修改
flag为
true。

在某些算法描述里,称有两个链表,一个为活链表(待访问),一个为死链表(访问完)。每一
次衍生状态时,先判断它是否已在两个链表中,若存在,则不衍生

;若不存在,将其放入活链表。
对于被衍生的那个状态,放入死链表。

为了记录每一个状态是否被访问过,我们需要有足够的空间。八数码问题一共有
9!,这个数字
并不是很大,但迎面而来的另一个问题是我们如何快速访问这些状态,如果是单纯用链表的话,那
么在规模相当大,查找的状态数目十分多的时候就不能快速找到状态,其复杂度为
O(n),为了解决这
个问题,本文将采用哈希函数的方法,使复杂度减为
O(1)。

这里的哈希函数是用能对许多全排列问题适用的方法。取
n!为基数,状态第
n位的逆序值为哈
希值第
n位数。对于空格,取其(9
位置)再乘以
8!。例如,137246858的哈希值等于:


0*0!
+0*1!+
0*2!+2*3!+
1*4!+1*5!
+0*6!+
3*7!+(98)*
8!=
55596<9!

具体的原因可以去查查一些数学书,其中
123456789的哈希值是
0最小,876543210
的哈希值是(9!1)
最大,而其他值都在
0到(
9!1)
中,且均唯一。


Q5:如何使搜索只求得最佳的解?

Q5:如何使搜索只求得最佳的解?

DFS(深度优先搜索)。除了
DFS,还有
BFS,从概念上讲,两者只是在扩展时
的方向不同,DFS向深扩张,而
BFS向广扩张。在八数码问题的解集树中,树的深度就表示了从初
始态到目标态的步数,DFS一味向深,所以很容易找出深度较大的解。


BFS可以保证解的深度最少,因为在未将同一深度的状态全部访问完前,BFS不会去访问更深
的状态,因此比较适合八数码问题,至少能解决求最佳解的难题。例如:


左图进行的是
BFS,起始状态
衍生四个状态,然后依次访问这四
个状态,将第
1层全访问完后再访
问第二层,直到找到
END状态为
止。

但是
BFS和
DFS一样不能解
决问题
c,因为每个状态都需要扩张,所以广搜很容易使待搜状态的数目膨胀。最终影响效率。


Q6:该如何减少因广搜所扩张的与目标状态及解无关的状态?

前面所说的都是从
START状态向
END状态搜索,那么,将
END状态与
START状态倒一下,
其实会有另一条搜索路径(Q8策略三讨论),但简单的交换
END与
START并不能缩小状态膨胀的
规模。我们可以将正向与反向的搜索结合起来,这就是双向广度搜索。

双向广搜是指同时从
START和
END两端搜,当某一端所要访问的一个状态是被另一端访问过
的时候,即找到解,搜索结束。它的好处是可以避免广搜后期状态的膨胀。可以用下面这张图形象
的进行比较:


左图的解集数是双向广搜的树,通过两端的共同搜索,两端找到了共同的状态。右端的树是同
一个问题的
BFS树,在搜索后期,规模不

断扩大,虽然能找到了目标状态,但其访问了许多无用结
点,浪费了空间和时间。

采用双向广度搜索可以将空间和时间节省一半!


Q7:决定一个快的检索策略?

双向广搜能大大减少时间和空间,但在有的情况下我们并不需要空间的节省,比如在
Q4中已
经决定了我们需要使用的空间是
9!,所以不需要节省。这样我们可以把重点放在时间的缩短上。

启发式搜索是在路径搜索问题中很实用的搜索方式,通过设计一个好的启发式函数来计算状态
的优先级,优先考虑优先级高的状态,可以提早搜索到达目标态的时间。
A*是一种启发式搜索的,
他的启发式函数
f'()=g'()+
h'()能够应用到八数码问题中来。


g'
()
从起始状态到当前状态的实际代价
g*()的估计值,g'
()>=
g*()
h'
()
从当前状态到目标状态的实际代价
h*()的估计值,h'
()<=
h*()
注意两个限制条件:


(1)h'
()<=
h*()(2)任意状态的
f
'()值必须大于其父状态的
f'()值,即
f'()单调递增。
其中,g'
()是搜索的深度,
h'
()则是一个估计函数,用以估计当前态到目标态可能的步数。解
八数码问题时一般有两种估计函数。比较简单的是
difference(Statusa,
Statusb),其返回值是
a和
b状态各位置上数字不同的次数。另一种比较经典的是曼哈顿距离
manhattan
(Status
a,
Status
b),
其返回的是各个数字从
a的位置到
b的位置的距离(见例子)。

例如状态
137246852和状态
123456789的
difference是
5(不含空格,图中红色)。

而他的
manhattan距离是:
1
(7d一次)+
1(2u一次)
+2
(4l两次)+3
(6r两次
u一次)
+2
(5u一次
l一次)=9
单个数字的
manhattan应该小于
5,因为对角的距离才
4,若大于
4则说明计算有误。


无论是
difference还是
manhattan,估计为越小越接近
END,所以优先级高。

在计算
difference和
manhattan时,推荐都将空格忽略,因为在
difference中空格可有可无,对整
体搜索影响不大。

考虑下面两个状态(左需要
3步到达
END态,右需要
4步到达
END态),不含空格时
diff=3
是相同的,含空格时左边的比右边的大一,但是实际上左图只要
3步就能到达
END态,而右图需要
4步,多计算了空格反而把优先级搞错了。


manhattan中,如果把空格也算上,实际上只会降低搜索速度(经过实验证明),同样考虑下
图,不计算空格时两者的
manhattan是左
3右
4,比
diff要好,因为左的优先级大于右了。而加上空
格后,manhattan是左
6右
4,不但颠倒了优先级,其误差比
diff加空格后还要大。


本文后面的实现将使用
manh

attan不计空格的方法。其实,每移动一步,不计空格,相当于移
动一个数字。如果每次移动都是完美的,即把一个数字归位,那么
START态到
END态的距离就是
manhattan。反过来说,manhattan是
START到
END态的至少走的步数。

回到
f'()=g'()+h'
(),其实广度搜索是
h'
()=0的一种启发式搜索的特例,而深度搜索是
f'
()=0
的一般搜索。h'
()对于优化搜索速度有很重要的作用。


Q8:能否进一步优化检索策略?

Q8:能否进一步优化检索策略?



A*搜索策略的优劣就是看
h'
()的决定好坏。前面列举了两个
h'()的函数,但光有这两个是不够
的。经过实验分析,在
f'()中,g
'()决定的是
START态到
END态中求得的解距离最优解的距离。而
h'
()能影响搜索的速度。

所以优化的第一条策略是,放大
h'
(),比如,让
h
'()=
10*
manhattan(),那么
f
'()=
g'
()+10*manhattan(),可能提高搜索速度。可惜的是所得的解将不再会是最优的了。

为什么放大
h'()能加快搜索速度,我们可以想象一下,h'()描述的是本状态到
END态的估计距离,
估计距离越短自然快一点到达
END态。而
g'
()描述的是目前的深度,放大
h'
()的目的是尽量忽略深
度的因素,是一种带策略的深搜,自然速度会比广搜和深搜都快,而因为减少考虑了深度因素,所
以离最优解就越来越远了。关于
h'
()放大多少,是很有趣的问题,有兴趣可以做些实验试试。

第二条是更新待检查的状态,由于
A*搜索会需要一个待检查的序列。首先,在
Q4已经提到用
哈希避免重复访问同一状态。而在待检查队列中的状态是未完成扩张的状态,如果出现了状态相同
但其
g'()比原
g'()出色的情况,那么我们更希望的是搜索新状态,而不是原状态。这样,在待检查队
列中出现重复状态时,只需更新其
g'()就可以了。

第三条是注意实现程序的方法,在起初我用
sort排序
f'()后再找出权值最大的状态,而后发现用
make_heap要更快。想一想,由于需要访问的接点较多,待访问队列一大那么自然反复排序对速度
会有影响,而堆操作则比排序更好。另一点是,实现更新待检查队列时的搜索也要用比较好的方法
实现。我在
JAVA的演示程序中用的
PriorityQueue,可是结果不是很令人满意。

第四条优化策略是使用
IDA*的算法,这是
A*算法的一种,ID名为
Iterativedeepening是迭代加
深的意思。思想是如下:

顺便准备一个记录一次循环最小值的
temp=MAX,
h'取
manhattan距离
先估算从
START态到
END态的
h'()记录为
MIN,将
START放入待访问队列
读取队列下一个状态,

到队列尾则
GOTO⑦

g'()>
MINGOTO


g'()+h'()
>MIN是否为真,真
GOTO⑥,否
GOTO

扩展该状态,并标记此状态已访问。找到
END态的话就结束该算法。GOTO

temp
=min(manhattan,
temp),GOTO

若无扩展过状态,MIN=temp(ID的意思在这里体现)从头开始循环
GOTO



第五条优化策略本身与搜索无关,在做题时也没能帮上忙,不过从理论上讲是有参考价值的。
记得
Q6中介绍的从
END开始搜起吗?如果我们的任务是对多个
START与
END进行搜索,那么我
们可以在每搜索完一次后记录下路径,这个路径很重要,因为在以后的搜索中如果存在
START和
END的路径已经被记录过了,那么可以直接调出结果。


END搜起,可以方便判断下一次的
START是否已经有路径到
END了。当前一次搜索完时,
其已访问状态是可以直接使用的,若
START不在其中,则从待访问的状态链表中按搜索策略找下一
个状态,等于接着上一次的搜索结果开始找。

之所以没能在速度上帮上忙,是因为这个优化策略需要加大
g'
()的比重,否则很容易出现深度
相当大的情况,由于前一次搜索的策略与下一次的基本无关,导致前一次的路径无法预料,所以就
会出现深度过大的情况。解决方法是加大
g'
()。

策略五类似给程序加一个缓冲区,避免重复计算。如果要做八数码的应用,缓冲区会帮上忙的。


Q10:怎样记录找到的路径?

Q10:怎样记录找到的路径?

DFS,
所以不能借助通过函数调用所是使用的程序栈。

我们可以手工加一个模拟栈。在
Q4中解决了哈希的问题,利用哈希表就能快速访问状态对应
的值,在这里,我们把原来的
bool值改为
char或其他能表示一次操作(至少需要
5种状态,除了
u
rld外还要能表示状态已访问)的值就行了。

在搜索到解时,记录下最后一个访问的状态值,然后从读取该状态对应的操作开始,就像栈操
作的退栈一样,不停往回搜,直到找到搜索起点为止。记录好栈退出来的操作值,就是一条路径。

算法设计:

基本设计(一个简单的类图如下)



ACM解题测试


Status表示可以不压缩(即直接用
char[]表示,因为看下来几个网站对内存要求都不高)。
哈希读取可以直接用
map(C++),HashMap(Java),速度不会慢的。
在每次取优先级最大的状态时,建议用
C++的
make_heap()。

检索策略及效果见下:
PKU
1077题:数据不强,用一点点技巧就能过
JAVA:
DFS,BFS超时
双向广搜
406MS,4904K

distance为
h'
()搜索
718MS,3056K
C++:双向广搜
46MS,
1252K
f
'
()=manhattan搜索
015MS,
276K左右



manhattan为
h'()搜索
265MS,376K

manhattan为
h'(),更新待检查队列
15MS,276K
IDA*
108MS,888K


杭州电子科技大学1043题:数据强了点
f
'
()=manhattan搜索
2031ms,676K
f'
()=g'()+manhattan搜索
4031ms,708K
f'
()=g'()+5*manhattan搜索
3766ms,708K
f'
()=g'()+10*manhattan搜索
2515ms,708K
f'
()=g'()+15*manhattan搜索
4297ms,704K
f'
()=g'()+20*manhattan搜索
4328ms,704K



ZJU
1217题:对时间好苛刻,在这里
makeheap()
能过
sort()就不能
_
o
只有纯以
manhattan为
f'
()搜索
AC了
00:05.07,900K

manhattan为
h'(),更新待检查队列
00:06.11,904K
惊讶
Ivan是如何以
00:00.11,396K过的(还有"绿野风烟")。


UVA
652题:看来功力不够啊~没有通过。

应用设计

首先,拒绝在
ACM试题中表现出色的
f
'
()=manhattan搜索,因为其搜索所得的结果太长了
(搜索深度过大),得不到最优解就宁可选择慢一点点的。
在我写的小八数码软件里,我用了普通广搜、双向广搜和
A*(f'()=g'
()+h'
())三个搜索
作比较。其中普通搜索的速度很慢,而且消耗内存较多。以下用三个搜索对


进行搜索,情况如下:




7万多节点,节点的膨胀
之大让人无法承受,所以除非拿广搜来做比较,否则不要拿它来做应用。

然后,广搜的规模比普通搜索少了近
70倍。在许多次测试后都显示,广搜的搜索节点数不
多,速度也很快,最重要的是它所求的一定是最优解。因此,比较适合拿来做应用。


A*表现也不错,由于是带有一种好的倾向,所以他搜索的节点十分少,也因为这样他的表
现也比较好。仔细的读者一定发现了,在数据统计中双向广搜和
A*的时间上相差无几。其原因
我分析是在用
A*搜索时的状态优先级的顺序是用
PriorityQueue(优先队列,插入复杂度
O(lgn),
移除和查找复杂度
O(n),出队列复杂度
O(1))来实现,加上更新待检查队列没办法很简单的实
现,所以在这些细节上还是有所消耗。最后,A*的解不是最优(最短的),但还是可以接受的。

对于
Q8中的第五条,我在
BFS和
A*中实现了,双端广搜因为结构上的因素没能实现。

程序实现:(JAVA)


相关链接:

相关链接:

BLOG(含本文及其他文章的收藏):https://www.360docs.net/doc/3a9175575.html,/ray58750034/
A*算法及其应用:https://www.360docs.net/doc/3a9175575.html,/blog/more.asp?name=qingshansima&id=4920
初识
A*算法:https://www.360docs.net/doc/3a9175575.html,/Articles/Program/Abstract/a8first.htm
深入
A*算法:https://www.360docs.net/doc/3a9175575.html,/AStart2.htm


Eight(ACM试题):
PKU
1077题(易):https://www.360docs.net/doc/3a9175575.html,/JudgeOnline/showproblem?problem_id=1077
ZJU
1217题(颇

难):https://www.360docs.net/doc/3a9175575.html,/show_problem.php?pid=1217
UVA
652题:
http://acm.uva.es/p/v6/652.html
杭州电子科技大学
1043题(难):https://www.360docs.net/doc/3a9175575.html,/showproblem.php?pid=1043
中国科技大学
112题(非
SJ):https://www.360docs.net/doc/3a9175575.html,/makeproblem.php?probID=112

相关题目:
SOJ
2061题:
https://www.360docs.net/doc/3a9175575.html,/soj/problem.action?id=2061
SOJ
1110题:
https://www.360docs.net/doc/3a9175575.html,/soj/problem.action?id=1110
SOJ
2360题:
https://www.360docs.net/doc/3a9175575.html,/soj/problem.action?id=2360




相关文档
最新文档