搞了一个通宵,终于搞定了!偶也!
Sicily 的 1379 那题,就是求给定的初始状态到目标状态,转换至少需要多少步。
例如状态
1 2 3
4 5 0
7 8 6
到达目标状态
1 2 3
4 5 6
7 8 0
只需一步(将 0 下移一格)即可。
在周赛的时候用广度搜索生成所有状态,耗时 0.6S 过了。但是这样不是聪明的办法(不过原理简单,实现也简单)。解这个问题的经典方法是使用 A* 算法进行启发式搜索,一般的人工智能原理的书都会介绍,在此不多说了。
看了下百度之星 2005 的比赛里 ACRush 的 代码,只看懂一小部分 orz... (注释都没有的!),不过也收获不少,hmhm。。也看了 Ray 的这篇 报告,受益匪浅。
下面是对该报告的一些
补充(最好自己先看看那篇报告和人工智能的书关于启发式搜索的那章):
此题关键之一是使用曼哈顿距离来作启发函数。两个状态中对应的数字的的横向距离和纵向距离之和,定义为该数字的距离,而除了 0 的各个数字的距离之和定义为两个状态的距离,如上面提及的两个状态的距离为 1(只有数字 6 位置不同,而横向距离为 0, 纵向距离为 1)。
不难证明两个状态之间转换所需的步数不少于它们的距离。这样就满足了一般的 A* 算法的要求。采用评价函数 f (state) = g (state) + distance (state, goalState),越小就越有希望到达目标状态,其中 g (state) 为从初始状态到达该状态的步数(随着搜索的进行,会发现到达该状态的更短的转换步骤,因此 g (state) 会逐渐减少至最小值 g* (state))。
从开始状态开始搜索,按照将 0 上下左右移动的四种方式生成新的状态(当然要考虑可行性,如 0 已在最右边就不能再右移了)。在新的状态中找出评价函数值 f (state) 最小的状态来继续搜索(我们需要维护一个最小优先级队列 -- OPEN 表 -- 来找出 f 值最小的状态),直至搜索到目标状态。如果两个状态是可以互相达到的,那么可证 A* 算法一定可以找到最优解。
不难证明曼哈顿距离满足单调性,即在状态转换 state 1 -> 2 -> 3 中,distance (state1, state3) < distance (state2, state3) + cost (state1, state2)。
在满足这样的条件下,我们的搜索将满足以下两个有趣且有用的特性:
当要选择一个状态生成新状态时,它的 g (state) = g* (state),即找到到达该状态的最少步数(如果你记录转换的上下左右记录的话,也将得到最少步数的转换步骤)。
选择的状态的评价函数值 f 是逐渐递增的。
如果不满足这个条件,那么在选择一个状态生成新状态时,新状态可能已经被生成过而且还被选择过,而达到这个新状态的步数可能会更少,即发现了更少的转换步骤,这样就有重复的工作了。而在满足条件下,一旦被选择则步数就已经是最少的了,就像最短路的 Dijkstra 算法那样。
而为了查询一个状态是否选择过,我们可以用一个 CLOSED 表 -- 来记录状态,在 C ++ 的 STL 中,我们可以选择 map 这种数据结构(一种平衡的二叉查找树),将一个状态对应的数字(如目标状态表示为 123456780)映射到对应的耗费 g 值。当然我们可以使用 hash_map 或者 trie 这两种数据结构,,或者自己实现一个哈希表,甚至使用“康托展开”直接映射到一个数组元素(后面几种数据结构占用内存较多)。这也是关键之一,我用自己实现的 trie 来做速度加快很多。另外预处理和使用一个变量 pos 来记录数字 0 的位置也很关键。
以下是我的代码:
//
八数码问题 A* 算法
//
by [email protected]
//
2007-12-10
#include
<
cstdio
>
#include
<
queue
>
#include
<
map
>
using
namespace
std;
const
int
ten [
9
]
=
{
100000000
,
10000000
,
1000000
,
100000
,
10000
,
1000
,
100
,
10
,
1
};
int
dist [
9
][
9
], diff [
9
][
9
];
//
dist [i][j] 为位置 i, j 的曼哈顿距离, diff [i][j] 为交换位置 i, j 使状态数改变的量
int
goalPos [
9
]
=
{
8
,
0
,
1
,
2
,
3
,
4
,
5
,
6
,
7
};
//
goalPos [i] 为目标状态中 i 的位置
int
goalNum
=
123456780
;
//
目标状态的状态数
int
goalInv
=
0
;
//
目标状态忽略 0 后的逆序数
int
state [
9
];
//
当前状态
struct
STATE{
int
num, pos, g, h;
//
状态数,0 的位置,达到此状态的耗费,到达目标状态的启发函数值
STATE (
int
num,
int
pos,
int
g,
int
h):num(num), pos(pos), g(g), h(h){}
bool
operator
<
(
const
STATE
&
other)
const
//
状态的评价函数等于耗费加上启发函数值
{
if
(g
+
h
==
other.g
+
other.h)
return
h
>
other.h;
//
由于查询较少,此句帮助不大,可删掉
return
g
+
h
>
other.g
+
other.h;
}
};
void
preprocess ()
//
预处理
{
for
(
int
i
=
0
; i
<
9
; i
++
){
dist [i][i]
=
0
;
for
(
int
j
=
0
; j
<
i; j
++
){
dist [i][j]
=
dist [j][i]
=
abs (i
/
3
-
j
/
3
)
+
abs (i
%
3
-
j
%
3
);
diff [i][j]
=
diff [j][i]
=
abs (ten [i]
-
ten [j]);
}
}
}
bool
noAns (
int
pos)
//
检查开始状态忽略 0 后的逆序数,如果和目标状态的逆序数奇偶性不一致,则没有解
{
int
inv
=
0
;
for
(
int
i
=
0
; i
<
9
; i
++
){
for
(
int
j
=
0
; j
<
i; j
++
){
if
(state [j]
>
state [i]) inv
++
;
}
}
return
(inv
-
pos
-
goalInv)
%
2
!=
0
;
}
int
heu (
int
pos)
//
计算启发函数值
{
int
h
=
0
;
for
(
int
i
=
0
; i
<
9
; i
++
){
int
j
=
goalPos [state [i]];
h
+=
dist [i][j];
}
return
h
-
dist [pos][goalPos [
0
]];
}
int
astar ()
{
//
int cnt = 0;
int
num
=
0
, pos
=
0
;
for
(
int
i
=
0
; i
<
9
; i
++
){
scanf (
"
%d
"
,
&
state [i]);
}
for
(
int
i
=
0
; i
<
9
; i
++
){
num
=
num
*
10
+
state [i];
}
for
(
int
i
=
0
; state [i]; i
++
){
pos
++
;
}
if
(noAns (pos))
return
-
1
;
//
检查是否无解
map
<
int
,
int
>
ng;
//
CLOSED 表,已扩展的结点(状态数 -> 到达该结点的最少耗费)
priority_queue
<
STATE
>
q;
//
OPEN 表,待扩展的结点,但是仍然会存在已扩展的结点的记录
STATE start(num, pos,
1
, heu (pos));
//
因为 map 对不存在的 key 返回 0,故初始状态的耗费应设为 1
q.push (start);
while
(q.size ()){
//
cnt ++;
STATE top
=
q.top ();
//
考察 OPEN 表中评价函数值最小的结点
q.pop ();
int
pos
=
top.pos, num
=
top.num, g
=
top.g, h
=
top.h;
if
(num
==
goalNum){
//
printf ("%d ", cnt);
return
g
-
1
;
//
找到最优解,注意要减去 1
}
if
(ng [num])
continue
;
//
已经扩展过此结点则忽略
ng [num]
=
g;
//
结点加入 CLOSED 表
//
扩展此结点
if
(pos
>
2
){
//
move 0 up
int
p
=
pos
-
3
;
int
i
=
num
/
ten [p]
%
10
, n
=
num
-
i
*
diff [pos][p];
int
h2
=
h
-
dist [p][goalPos [i]]
+
dist [pos][goalPos [i]];
if
(
!
ng [n]) q.push (STATE (n, p, g
+
1
, h2));
}
if
(pos
<
6
){
//
move 0 down
int
p
=
pos
+
3
;
int
i
=
num
/
ten [p]
%
10
, n
=
num
+
i
*
diff [pos][p];
int
h2
=
h
-
dist [p][goalPos [i]]
+
dist [pos][goalPos [i]];
if
(
!
ng [n]) q.push (STATE (n, p, g
+
1
, h2));
}
if
(pos
%
3
){
//
move 0 left
int
p
=
pos
-
1
;
int
i
=
num
/
ten [p]
%
10
, n
=
num
-
i
*
diff [pos][p];
int
h2
=
h
-
dist [p][goalPos [i]]
+
dist [pos][goalPos [i]];
if
(
!
ng [n]) q.push (STATE (n, p, g
+
1
, h2));
}
if
(pos
%
3
!=
2
){
//
move 0 right
int
p
=
pos
+
1
;
int
i
=
num
/
ten [p]
%
10
, n
=
num
+
i
*
diff [pos][p];
int
h2
=
h
-
dist [p][goalPos [i]]
+
dist [pos][goalPos [i]];
if
(
!
ng [n]) q.push (STATE (n, p, g
+
1
, h2));
}
}
return
0
;
}
int
main ()
{
//
freopen ("input.txt", "r", stdin);
preprocess ();
int
t;
scanf (
"
%d
"
,
&
t);
while
(t
--
){
printf (
"
%d
"
, astar ());
}
return
0
;
}
/*
Input
第一行是一个整数n,表示一共有多少组测试数据。
下面n行每行9个字符,用空格隔开,代表一个初始状态。
目标状态是 1 2 3 4 5 6 7 8 0。
Output
最小操作步数,无解时输出-1。
sample input
4
1 2 3 4 5 0 7 8 6
1 2 3 4 0 5 6 7 8
8 6 7 2 5 4 3 0 1
6 4 7 8 5 0 3 2 1
sample output
1
14
31
31
注意在 Release 模式下运行此程序,否则可能会很慢!
注意优先队列 priority_queue 是最大堆,因此在定义 STATE 结构的比较函数时要让 f 值较小的元素较大,
而当 f 相等的时候,启发函数 h 较小的状态应该优先考虑,故让该状态较大。
优先队列中仍然会存在一些 CLOSED 表中的状态,因为在生成新的状态时没有查找删除该新状态是否生成过。
*/