一、实验题目
1.A*算法解决15-puzzle问题
2.IDA*算法解决15-puzzle问题
二、实验内容+结果分析
1.算法原理
(1)A*算法是一致代价搜索算法的升级版本,引入了一个启发式函数h(n),表示从当前状态到目标状态的距离或者难易程度,评价函数f(n)=g(n)+h(n)
,其中g(n)是初始状态到当前状态的步数。综合h和g能够更好地选择下一步需要扩展的点。
(2)IDA*是迭代加深算法的升级版本,不再是迭代加深那样盲目的搜索,而是和A *算法一样引入了启发式函数,让DFS算法扩展点时更具有目的性。
2.过程分析
A*:一开始先将初始状态加入到优先队列中备选,然后循环开始,每次从优先队列中取出一个f值最小的状态,将其加入到close
表中,表示已经扩展过这个节点。然后用这个状态产生新的状态,如果新状态未被扩展,则将其加入到优先队列中备选。直到从优先队列中找到的状态是目标状态,循环结束。
路径记录:用两个列表:list
和pre
分别按顺序记录访问过的状态和该状态的前一个状态,每次从优先队列中取出一个状态时,将其状态记录到list
中,将其前一个状态记录到pre
中。目标节点被扩展后,从list
中通过pre
中所记录的前驱状态一路往前寻找,直到初始状态,将路径反过来输出就是正确答案。
伪代码:
# A*
while not q.empty():
t = q.pop()
if 是目标状态:
break
更新t状态往上下左右走产生的新状态
if new_status not in close:
h_new = get_h(new_status)
t_new = (f_new,h_new,g_new,new_status)
close.add(new_status)
q.push(t_new)
i = pre[-1]
path = []
path.append(list[-1])
while i!=-1:
path.append(list[i])
i = pre[i]
for i in range(0,len(path)):
print("step:%d"%i)
print_puzzle(path[i])
IDA*:IDA *在迭代的过程中,不需要像A *那样储存所有可能扩展到的状态,维护一个很大的优先队列,而是只需要在当前状态下选择一种h值最小的状态就好。在迭代过程中,可以对一些不可能在当前max_dep下找到解的状态进行剪枝,即当h+now_dep > max_dep
时,走这条路径肯定无法在max_dep步内走到目标状态,所以不需要考虑这一步。
路径检测:在访问过当前状态后,从当前状态的优先队列中选取下一步状态前,将当前状态加入到close
表和path
表中。而若是迭代过程中,这个状态并不能找到目标状态时,则将close
表和path
表中相应的状态删除。到最后找到目标状态时,path
表中的状态一定是有序的,只需要按顺序输出path
表中0~len-1个元素就行。
伪代码:
# IDA*
def IDA(close,path,puzzle,max_dep,now_dep):
if max_dep<now_dep:
return 0
if puzzle==goal:
return 1
is_find = 0
q = PriorityQueue()
上下左右走以产生新状态
# 对于h比剩余步数还要大的状态,要进行剪枝
if new_status not in close and h(new_status)+now_dep<max_dep:
q.push(new_status)
while q and not is_find:
t = q.pop()
close.add(t.status)
path.append(t.status)
is_find = IDA(close,path,t.status,max_dep,now_dep+1)
max_dep = 10
is_find = IDA(close,path,puzzle,max_dep,0)
while is_find:
max_dep += 1
is_find = IDA(close,path,puzzle,max_dep,0)
for i in range(0,len):
print("step:%d"%i)
print_puzzle(path[i])
3.做一个性能怪
这里先把下面测试性能的测试样例放在这里:
可优化的点:
1)启发式函数
启发式函数1.0:错位格子数量。一开始,我使用的启发式函数是错位的格子数量。这种启发式函数很简单,但是由于其h的取值范围只有0-15(算上0的话就是16),取值范围太小,完全不足以描述出当前状态要达到目标状态的难易程度。
举个例子:
两个状态的启发值都是3(不算0),但是很显然,上面状态相较于下面状态来说,要更容易达到目标状态。因此选用错位格子个数作为启发式函数是效率很低的。这个启发式函数实现很简单而且很没用,所以不附代码了。
用这个启发式函数跑样例三、四,内存爆了都跑不出来。
启发式函数2.0:曼哈顿距离。曼哈顿距离是当前位置和目标位置的横纵距离差值之和,而另一种启发式函数则可以采用1-15的总曼哈顿距离。这种启发式函数的优点较启发式1.0的优点很明显,可以描绘出每个点到目标位置需要移动的步数,同时这种启发式函数是具有单调性的,每个点到目标位置的步数之和肯定比实际上把所有点全部归位的步数要少的多。
代码:
def get_h(mp):
h1 = 0
for i in range(4):
for j in range(4):
if mp[i * 4 + j] != 0:
row_goal = (mp[i * 4 + j] - 1) // 4
col_goal = (mp[i * 4 + j] - 1) % 4
h1 += abs(i - row_goal) + abs(j - col_goal)
return h1
样例三结果展示:
样例四:
启发式函数3.0:曼哈顿距离+线性冲突法。这比启发式函数2.0多了一个线性冲突法。所谓的线性冲突法,就是一个格子在移向它目标位置的沿途中,对沿途格子的影响。举个例子:
当2已经移到了它的目标位置,但是它右边的1的目标位置在它的左边,1想要移到它的目标位置的话,要不就得从下面走,要不就得让2给它让路,这样2的位置则又乱了。所以这种情况就说明单单采用总的曼哈顿距离是不够准确的,加上线性冲突法能使启发式函数更接近于实际要走的步数。
采用这种方法能很大程度的提高A*扩展点的效率。
代码:
def get_h(mp):
h1 = 0
h2 = 0
# 添加线性冲突优化曼哈顿距离
for i in range(4):
for j in range(4):
if mp[i * 4 + j] != 0:
row_goal = (mp[i * 4 + j] - 1) // 4
col_goal = (mp[i * 4 + j] - 1) % 4
h1 += abs(i - row_goal) + abs(j - col_goal)
if j == col_goal:
for k in range(j + 1, 4):
if mp[i * 4 + k] != 0 and (mp[i * 4 + k] - 1) // 4 == i and (
mp[i * 4 + k] - 1) % 4 < j: # 同一行的右边某个点正确位置是在他的正左边
h2 += 2
if i == row_goal:
for k in range(i + 1, 4):
if mp[k * 4 + j] != 0 and (mp[k * 4 + j] - 1) % 4 == j and (
mp[k * 4 + j] - 1) // 4 < i: # 同一列的下边某个点正确位置是在他的正上边
h2 += 2
return h1 + h2
样例三结果:
样例四结果:
直观的看,启发式函数1.0到3.0的效率提升:
样例三: I N F → 6 s → 3 s INF \rightarrow 6s \rightarrow 3s INF→6s→3s
样例四: I N F → 45 s → 15 s INF \rightarrow 45s \rightarrow 15s INF→45s→15s
2)数据结构
close表:我之前用的是列表储存close表,但是在每次扩展新状态的时候,都需要访问close表查找这个状态是否已经在close表中,用列表来储存的话访问的效率太低。因此最合适的使用集合set
来储存close,访问效率很高。
状态:这个也可以用列表来实现,但是由于需要比较当前状态和目标状态,因此使用元组tuple
来储存状态信息比较的效率会更高。
优先队列:可以使用库里的PriorityQueue
或者heapq
来实现。在数据量大的时候,使用heapq
堆排序来构建优先队列效率会更高。heapq
库主要使用的函数:
heapq.heappush(q,item) # 压入队列
item = heapq.heappop(q) # 弹出队列
深拷贝or切片:在一开始写的时候,当需要通过移动当前状态来产生新状态时,我自然而然的使用了深拷贝来产生新状态。但是切片的效率要比深拷贝快的多,而切片又不能使用在二维列表中。这就是我为什么要用一维列表来储存puzzle状态而不是用4*4的二维列表来储存。一维列表不仅能直接将puzzle直接转化为元组进行高效地比较,也能直接使用切片产生新的状态。
三、结果展示
1.A*算法
样例一:
样例二:
样例三:
样例四:
样例五:
样例六:
2.IDA*算法
样例一:
样例二:
样例三:
样例四:
样例五:
样例六:
统计
样例 | A*耗时 | IDA*耗时 |
---|---|---|
1 | 0.005 s | 0.003 s |
2 | 0.0009 s | 0.001 s |
3 | 3 s | 21 s |
4 | 15 s | 82 s |
5 | 675 s | 6430 s |
6 | 578 s | 6168 s |