AI Lab2: 启发式搜索

AI Lab2: 启发式搜索

梦猫 Lv2

一、实验要求

这次实验要求我们用A算法求解八数码问题。
八数码问题是指,在一个3*3的棋盘上,存在8个棋子和1个空位,其中每个棋子都可以向上下左右四个方向移动(若存在空位)。给定一个初始状态和目标状态,要求找到从初始状态变换至目标状态的最短路径,输出步数和路径中的状态。

8-puzzle
8-puzzle

而启发式搜索算法则是一种状态空间树搜索算法。我们首先根据问题定义一个启发函数h,将h的值作为当前状态与目标状态之间的“差距”,用以判断当前状态的“好坏”。随后,我们对所有未扩展状态进行按估值h进行排序,每次都扩展当前最“好”的状态,直至找到目标状态,或是完全遍历状态空间树(无解)。
heuristic-algorithm
heuristic-algorithm

二、算法设计

A算法最核心的地方就在于针对具体问题设计良好的启发函数h,剩下只需要用一个优先级队列存储所有未扩展状态,按h大小排序,挨个扩展并计算搜索深度(即步数)即可。因为这次实验要求我们给出结果路径的中间状态,所以还需要把已扩展节点也进行储存,并且在扩展过程中保留好路径信息。
估值函数上,这次实验给了四种h函数的备选,我们从中选择一个进行实现即可。

heuristic-function
heuristic-function

我选了曼哈顿距离和(启发函数二)作为启发函数,曼哈顿距离是指两个点坐标之间的绝对轴距之和,是一种很简单但很经典的启发函数。我们只需计算当前状态中每个点与目标状态中对应点的曼哈顿距离之和,便可以得到当前状态的估值h,h值越小代表状态与目标状态越接近,也就是状态越“好”。
Manhattan-distance
Manhattan-distance

算法设计完成,下面就可以Coding了(这次还是用了C++)。

Define

首先是一些定义和初始化工作。定义状态结构体Node,存储对应的棋盘状态,估值h,步数depth。同时,因为对输出路径的要求,还需要存储状态id,以及其父状态parent,用于回溯求解路径。随后,我们需要定义用于优先级队列排序的比较函数,定义曼哈顿距离h更小的状态更优,而在h相等的情况下,我们应优先选择步数更少的状态。同时,我们定义存储未扩展节点的优先级队列open,用于回溯路径的已扩展节点序列close和路径path。最后,我们还需定义目标状态中各点的坐标。

DEFINE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
struct Node {
int map[3][3];
int h, depth;
int id, parent;
};

struct cmp {
bool operator()(Node a, Node b) {
if(a.h > b.h)
return 1;
else if(a.h < b.h)
return 0;
else
return a.depth > b.depth;
}
};

// Node.h最小堆
priority_queue<Node, vector<Node>, cmp> open;
vector<Node> close;
vector<Node> path;

// 目标状态
// 1 2 3
// 8 0 4
// 7 6 5

int end_loc[8][2] = {{0, 0}, {0, 1}, {0, 2}, {1, 2}, {2, 2}, {2, 1}, {2, 0}, {1, 0}};

Manhattan Distance

下面根据曼哈顿距离算法编写启发函数h。计算map中除0(即空位)外所有点与目标状态下的曼哈顿距离,并不断加和即可。

ManhDis
1
2
3
4
5
6
7
8
9
10
11
12
// 计算当前状态的曼哈顿距离
int H(int map[3][3]) {
int sum = 0;
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
int num = map[i][j];
if (num == 0) continue;
sum += abs(i - end_loc[num - 1][0]) + abs(j - end_loc[num - 1][1]);
}
}
return sum;
}

Other Functions

随后再定义一些辅助函数。例如checkSame函数,用于检验当前状态是否在open或close序列中存在,以避免对状态的重复搜索。以及print_info函数(可选),用于在搜索过程中输出每次扩展的状态信息,辅助调试。

HELPER
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 检查是否存在已搜索的相同状态
bool checkSame(Node t) {
for (int i = 0; i < close.size(); i++) {
bool same = 1;
for (int j = 0; j < 3; j++) {
for (int k = 0; k < 3; k++) {
if (close[i].map[j][k] != t.map[j][k]) {
same = 0;
break;
}
}
}
if (same)
return 1;
}
return 0;
}

void print_info(Node cur) {
cout<<"当前状态:"<<endl;
for(int i=0;i<3;i++){
for(int j=0;j<3;j++){
cout<<cur.map[i][j]<<" ";
}
cout<<endl;
}
//id
cout<<"id: "<<cur.id<<endl;
//parent
cout<<"parent: "<<cur.parent<<endl;
//depth
cout<<"depth: "<<cur.depth<<endl;
//h
cout<<"曼哈顿距离:"<<cur.h <<endl;
cout<<endl;
}

Init

下面进入main函数。首先是初始化部分,要求用户输入初始状态(以数字0表示空位),设置其相关信息,并将初始状态加入未扩展节点列表。

INIT
1
2
3
4
5
6
7
8
9
10
11
12
13
Node start;
cout<<"请输入初始状态:"<<endl;
for(int i=0;i<3;i++){
for(int j=0;j<3;j++){
cin>>start.map[i][j];
}
}

start.h = H(start.map);
start.depth = 0;
start.id = 0;
start.parent = -1;
open.push(start);

随后,开始对状态空间树的搜索。我们从open队列中取出最优的状态,检查其是否为目标状态(后续给出)后,尝试对其进行变换,搜索其子状态。对于状态的变换,虽然实际的定义是尝试让每个棋子向上下左右四个方向移动,但由于只有移动至空格位置才能变换成功,因此可以等效为尝试将空格进行上下左右四个方向(处于边界则移动失败)的移动。因此,我们可以分别尝试四种移动。对于每次变换后的子状态,我们首先更新其棋盘,并根据棋盘进行相同状态检索。若该状态已经检索过,则跳过。否则,我们应计算其估值,步数,id等信息,并记录其父状态的id,以便后续回溯求解路径。最后,我们将该状态加入未扩展状态队列open中,并继续搜索下一子状态。

SEARCH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
Node cur = open.top();
open.pop();
close.push_back(cur);

// print_info(cur);

int x, y;
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
if (cur.map[i][j] == 0) {
x = i;
y = j;
break;
}
}
}

if (x > 0) {
Node next = cur;
next.map[x][y] = cur.map[x - 1][y];
next.map[x - 1][y] = 0;
// 遍历close表和open,若存在相同状态则跳过
bool flag = 0;
flag = checkSame(next);
if (!flag) {
next.h = H(next.map);
next.depth = cur.depth + 1;
next.id = ++count;
next.parent = cur.id;
open.push(next);
}
}

if (x < 2) {
Node next = cur;
next.map[x][y] = cur.map[x + 1][y];
next.map[x + 1][y] = 0;
bool flag = 0;
flag = checkSame(next);
if (!flag) {
next.h = H(next.map);
next.depth = cur.depth + 1;
next.id = ++count;
next.parent = cur.id;
open.push(next);
}
}

if (y > 0) {
Node next = cur;
next.map[x][y] = cur.map[x][y - 1];
next.map[x][y - 1] = 0;
bool flag = 0;
flag = checkSame(next);
if (!flag) {
next.h = H(next.map);
next.depth = cur.depth + 1;
next.id = ++count;
next.parent = cur.id;
open.push(next);
}
}

if (y < 2) {
Node next = cur;
next.map[x][y] = cur.map[x][y + 1];
next.map[x][y + 1] = 0;
bool flag = 0;
flag = checkSame(next);
if (!flag) {
next.h = H(next.map);
next.depth = cur.depth + 1;
next.id = ++count;
next.parent = cur.id;
open.push(next);
}
}

Check

前面提到,我们在取出一个未扩展状态后需要先检查其是否为目标状态,随后再进行扩展,现在就对检查进行讨论。当状态估值h为0时,代表状态为目标状态。因此,我们可以认定找到一个解,并输出步数和搜索空间(可指示算法运行时间)。而对于路径的回溯,我们需要从最终的状态开始,不断根据状态的parent属性,在close序列中查找id等于parent的状态,并将其加入path序列中。最后倒序输出path即为我们搜索到的求解路径。

CHECK
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
if (cur.h == 0) {
cout<<"找到解!"<<endl;
cout<<"步数:"<<cur.depth<<endl;
cout<<"搜索空间:"<<count<<endl;
cout<<"路径:"<<endl;
path.push_back(cur);
while (cur.parent != -1) {
for (int i = 0; i < close.size(); i++) {
if (close[i].id == cur.parent) {
path.push_back(close[i]);
cur = close[i];
break;
}
}
}
for (int i = path.size() - 1; i >= 0; i--) {
cout<<"第 " <<path.size() - i - 1 <<" 步:\n";
for(int j=0;j<3;j++){
for(int k=0;k<3;k++){
cout<<path[i].map[j][k]<<" ";
}
cout<<endl;
}
cout<<"曼哈顿距离:" <<path[i].h <<endl;
cout<<endl;
}
break;
}

三、程序测试

实验手册给出了两个测试样例,涵盖了步数较少和较多两种情况,我就直接拿来用了。

test-1
test-1

test-2
test-2

第二个样例的路径较长,就不全放了。

四、实验总结

这次实验的算法也比较简单,编码上搜索状态空间树的部分有点复杂,但难度也不大。而A算法最核心的就是启发函数的设计,实验直接给了4个函数备选,因此也没什么设计难度,编码实现即可。

五、源码

最后放一下全部代码。

CODE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
#include <iostream>
#include <vector>
#include <cstdlib>
#include <queue>
#include <cmath>

using namespace std;

struct Node {
int map[3][3];
int h, depth;
int id, parent;
};

struct cmp {
bool operator()(Node a, Node b) {
if(a.h > b.h)
return 1;
else if(a.h < b.h)
return 0;
else
return a.depth > b.depth;
}
};

// Node.h最小堆
priority_queue<Node, vector<Node>, cmp> open;
vector<Node> close;
vector<Node> path;

// 目标状态
// 1 2 3
// 8 0 4
// 7 6 5

int end_loc[8][2] = {{0, 0}, {0, 1}, {0, 2}, {1, 2}, {2, 2}, {2, 1}, {2, 0}, {1, 0}};


// 计算当前状态的曼哈顿距离
int H(int map[3][3]) {
int sum = 0;
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
int num = map[i][j];
if (num == 0) continue;
sum += abs(i - end_loc[num - 1][0]) + abs(j - end_loc[num - 1][1]);
}
}
return sum;
}

// 检查是否存在已搜索的相同状态
bool checkSame(Node t) {
for (int i = 0; i < close.size(); i++) {
bool same = 1;
for (int j = 0; j < 3; j++) {
for (int k = 0; k < 3; k++) {
if (close[i].map[j][k] != t.map[j][k]) {
same = 0;
break;
}
}
}
if (same)
return 1;
}
return 0;
}

void print_info(Node cur) {
cout<<"当前状态:"<<endl;
for(int i=0;i<3;i++){
for(int j=0;j<3;j++){
cout<<cur.map[i][j]<<" ";
}
cout<<endl;
}
//id
cout<<"id: "<<cur.id<<endl;
//parent
cout<<"parent: "<<cur.parent<<endl;
//depth
cout<<"depth: "<<cur.depth<<endl;
//h
cout<<"曼哈顿距离:"<<cur.h <<endl;
cout<<endl;
}

int main() {
Node start;
cout<<"请输入初始状态:"<<endl;
for(int i=0;i<3;i++){
for(int j=0;j<3;j++){
cin>>start.map[i][j];
}
}

start.h = H(start.map);
start.depth = 0;
start.id = 0;
start.parent = -1;
open.push(start);

int count = 0;
while (!open.empty()) {
Node cur = open.top();
open.pop();
close.push_back(cur);

// print_info(cur);

if (cur.h == 0) {
cout<<"找到解!"<<endl;
cout<<"步数:"<<cur.depth<<endl;
cout<<"搜索空间:"<<count<<endl;
cout<<"路径:"<<endl;
path.push_back(cur);
while (cur.parent != -1) {
for (int i = 0; i < close.size(); i++) {
if (close[i].id == cur.parent) {
path.push_back(close[i]);
cur = close[i];
break;
}
}
}
for (int i = path.size() - 1; i >= 0; i--) {
cout<<"第 " <<path.size() - i - 1 <<" 步:\n";
for(int j=0;j<3;j++){
for(int k=0;k<3;k++){
cout<<path[i].map[j][k]<<" ";
}
cout<<endl;
}
cout<<"曼哈顿距离:" <<path[i].h <<endl;
cout<<endl;
}
break;
}

int x, y;
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
if (cur.map[i][j] == 0) {
x = i;
y = j;
break;
}
}
}

if (x > 0) {
Node next = cur;
next.map[x][y] = cur.map[x - 1][y];
next.map[x - 1][y] = 0;
// 遍历close表和open,若存在相同状态则跳过
bool flag = 0;
flag = checkSame(next);
if (!flag) {
next.h = H(next.map);
next.depth = cur.depth + 1;
next.id = ++count;
next.parent = cur.id;
open.push(next);
}
}

if (x < 2) {
Node next = cur;
next.map[x][y] = cur.map[x + 1][y];
next.map[x + 1][y] = 0;
bool flag = 0;
flag = checkSame(next);
if (!flag) {
next.h = H(next.map);
next.depth = cur.depth + 1;
next.id = ++count;
next.parent = cur.id;
open.push(next);
}
}

if (y > 0) {
Node next = cur;
next.map[x][y] = cur.map[x][y - 1];
next.map[x][y - 1] = 0;
bool flag = 0;
flag = checkSame(next);
if (!flag) {
next.h = H(next.map);
next.depth = cur.depth + 1;
next.id = ++count;
next.parent = cur.id;
open.push(next);
}
}

if (y < 2) {
Node next = cur;
next.map[x][y] = cur.map[x][y + 1];
next.map[x][y + 1] = 0;
bool flag = 0;
flag = checkSame(next);
if (!flag) {
next.h = H(next.map);
next.depth = cur.depth + 1;
next.id = ++count;
next.parent = cur.id;
open.push(next);
}
}
}
}
  • Title: AI Lab2: 启发式搜索
  • Author: 梦猫
  • Created at : 2024-05-14 10:52:20
  • Updated at : 2024-05-24 13:35:20
  • Link: https://mengmaor.github.io/2024/05/14/启发式搜索/
  • License: All Rights Reserved © 梦猫