这篇是https://blog.csdn.net/kengxie/article/details/119257159后继,需要先看看之前的文章才明白这里讨论解决的问题。
用A*算法解决翻箱问题的时候,性能是个特别大的问题,如果按之前的方式实现,直接用list保存open list和close list,遇到类似下面这样极端的情况,半个小时都跑不出结果。
0 0 0 0
9 10 11 12
5 6 7 8
1 2 3 4
在得出最终结果之前,算法的循环判断次数将超过7万次,open list中的元素会超过21万个。而且每个元素都可以看成S*T的二维数据,这里S代表堆放的排数量,T代表堆放的高度上限。标准的list在处理21万个S*T的二维数组的时候完全力不从心。
为了改善执行的效率,我们为翻箱步骤定制一个StepList类。主要的优化点有两个:
1. 类似Hashmap的实现方式,用Step的估值函数当dict的key,把估值相同的Step实例保存在对应的list中,list则作为dict的value保存。目的是快速找到估值最小的Step作为翻箱的下一步动作。
2. 借鉴字符串快速检索的方法,将每个Step的堆场状态进行预处理,保证无论open list中有多少个元素,按堆场状态查找Step的时候,最多只需要S*T次比较。具体实现方法是:
把container[0][0] -> container[0][1] -> ... -> container[S][T]按树状方式串联到一起。
查询的时候,以container[0][0]作为根逐级向下查看,如果叶子节点存在,那么叶子节点保存的对象就是查找的结果,如果叶子节点不存在,那么对应的Step不存在。
class StepList(object):
def __init__(self, values=None):
super().__init__()
self.linked_index = {}
self.sorted_steps = {}
self.min_step = None
self.length = 0
if values is not None:
self.append(values)
def __len__(self) -> int:
return self.length
# Mixin methods
def append(self, value: Step) -> None:
self.__add_item(value)
def remove(self, value: Step) -> None:
self.__del_item(value)
def get_item(self, value: Step):
row = value.stack.total_row
height = value.stack.max_height
current_index = self.linked_index
for i in range(row):
for j in range(height):
if value.stack.containers[i][j] in current_index:
current_index = current_index[value.stack.containers[i][j]]
else:
return None
return current_index['step']
def next_step(self):
return self.min_step
def __add_item(self, value: Step):
row = value.stack.total_row
height = value.stack.max_height
current_index = self.linked_index
for i in range(row):
for j in range(height):
current_index = current_index.setdefault(value.stack.containers[i][j], {})
current_index['step'] = value
self.length += 1
if value.f() not in self.sorted_steps:
self.sorted_steps[value.f()] = []
self.sorted_steps[value.f()].append(value)
if self.min_step is None or value.f() < self.min_step.f():
self.min_step = value
def __del_item(self, value: Step):
row = value.stack.total_row
height = value.stack.max_height
current_index = self.linked_index
for i in range(row):
for j in range(height):
current_index = current_index[value.stack.containers[i][j]]
current_index.clear()
self.length -= 1
steps = self.sorted_steps[value.f()]
steps.remove(value)
if len(steps) == 0:
del self.sorted_steps[value.f()]
if self.min_step == value:
self.min_step = self.sorted_steps[min(self.sorted_steps.keys())][0]
用这个类替代默认的list类,最后耗时大概4分20秒左右:
0 0 0 0
9 10 11 12
5 6 7 8
1 2 3 4
(11: (2, 2) -> (1, 3), 0, 7.3125)
0 11 0 0
9 10 0 12
5 6 7 8
1 2 3 4
(7: (2, 1) -> (0, 3), 1, 8.5625)
7 11 0 0
9 10 0 12
5 6 0 8
1 2 3 4
(3: (2, 0) -> (3, 3), 2, 10.8125)
7 11 0 3
9 10 0 12
5 6 0 8
1 2 0 4
(11: (1, 3) -> (2, 0), 3, 10.3125)
7 0 0 3
9 10 0 12
5 6 0 8
1 2 11 4
(10: (1, 2) -> (2, 1), 4, 10.375)
7 0 0 3
9 0 0 12
5 6 10 8
1 2 11 4
(6: (1, 1) -> (2, 2), 5, 10.625)
7 0 0 3
9 0 6 12
5 0 10 8
1 2 11 4
(2: (1, 0) -> (2, 3), 6, 11.875)
7 0 2 3
9 0 6 12
5 0 10 8
1 0 11 4
(7: (0, 3) -> (1, 0), 7, 11.5625)
0 0 2 3
9 0 6 12
5 0 10 8
1 7 11 4
(3: (3, 3) -> (1, 1), 8, 11.8125)
0 0 2 0
9 0 6 12
5 3 10 8
1 7 11 4
(2: (2, 3) -> (1, 2), 9, 12.875)
0 0 0 0
9 2 6 12
5 3 10 8
1 7 11 4
(12: (3, 2) -> (0, 3), 10, 13.25)
12 0 0 0
9 2 6 0
5 3 10 8
1 7 11 4
(8: (3, 1) -> (1, 3), 11, 14.5)
12 8 0 0
9 2 6 0
5 3 10 0
1 7 11 4
(4: (3, 0) -> (2, 3), 12, 15.75)
12 8 4 0
9 2 6 0
5 3 10 0
1 7 11 0
(12: (0, 3) -> (3, 0), 13, 15.25)
0 8 4 0
9 2 6 0
5 3 10 0
1 7 11 12
(9: (0, 2) -> (3, 1), 14, 15.4375)
0 8 4 0
0 2 6 0
5 3 10 9
1 7 11 12
(8: (1, 3) -> (3, 2), 15, 15.5)
0 0 4 0
0 2 6 8
5 3 10 9
1 7 11 12
(5: (0, 1) -> (3, 3), 16, 15.6875)
0 0 4 5
0 2 6 8
0 3 10 9
1 7 11 12
It could be done in 17 steps.
Start Time = 14:53:02 End Time = 14:57:21
完整代码如下:
from datetime import datetime
import operator
S = 4
T = 4
# 每一排箱子的发箱顺序
origin_stack = [[1, 5, 9, 0], [2, 6, 10, 0], [3, 7, 11, 0], [4, 8, 12, 0]]
# 代表堆场的类
class Stack(object):
def __init__(self, values, total_row, max_height):
# 所有箱子
self.containers = values
# 一共多少排
self.total_row = total_row
# 最大高度
self.max_height = max_height
# 当前各排实际高度
self.height = [max_height] * total_row
for i in range(total_row):
self.height[i] = -1
for j in range(max_height):
if values[i][j] != 0:
self.height[i] = j
# 取得某一排最上面一个箱子
def get_top_container(self, row):
if self.height[row] != -1:
return self.containers[row][self.height[row]]
else:
return None
# 修改某个位置箱子的发箱顺序
def set_container(self, position, value):
self.containers[position[0]][position[1]] = value
# 取得指定位置的箱子
def get_container(self, position):
return self.containers[position[0]][position[1]]
# 列举当前有哪些可移动步骤:只有最顶上的箱子可以被移动到其它有空位的排的顶上
def list_available_steps(self, close_steps):
available_steps = []
for i in range(self.total_row):
if self.height[i] < 0:
continue
for j in range(self.total_row):
if i != j and self.height[j] < self.max_height - 1:
cloned_stack = self.__copy__()
top_container = cloned_stack.get_top_container(i)
new_step = Step(top_container, (i, self.height[i]), (j, self.height[j] + 1), cloned_stack)
if close_steps.get_item(new_step) is None:
available_steps.append(new_step)
return available_steps
# 取得所有摆放位置不对的箱子
def get_incorrect_containers(self, row=-1):
if row == -1:
return self._get_incorrect_containers(range(self.total_row))
else:
return self._get_incorrect_containers(range(row, row+1))
# 取得所有摆放位置不对的箱子:如果发箱顺序小的箱子位于发箱顺序大的箱子之下,那么认为这个发箱顺序小的箱子向上的所有箱子位置都不对
def _get_incorrect_containers(self, rows):
result = []
for i in rows:
for j in range(1, self.max_height):
if self.containers[i][j] > self.containers[i][j-1]:
for k in range(j, self.max_height):
if self.containers[i][k] != 0:
result.append(self.containers[i][k])
break
return result
# 把箱子从move_from位置移动到move_to位置
def move(self, move_from, move_to):
self.set_container(move_to, self.get_container(move_from))
self.set_container(move_from, 0)
self.height[move_from[0]] -= 1
self.height[move_to[0]] += 1
# 打印当前堆场
def print(self):
for j in range(self.max_height):
for i in range(self.total_row):
print(str(self.containers[i][-j - 1]).rjust(4), end='\t')
print()
# 判断两个堆场是否相等:比较所有位置的箱子,如果所有箱子的发箱顺序一样,则相等
def __eq__(self, other):
return operator.eq(self.containers, other.containers)
# 深度复制堆场
def __copy__(self):
cloned_containers = [[None for _ in range(self.max_height)] for _ in range(self.total_row)]
for i in range(self.total_row):
for j in range(self.max_height):
cloned_containers[i][j] = self.containers[i][j]
return Stack(cloned_containers, self.total_row, self.max_height)
# 代表移动步骤的类
class Step(object):
def __init__(self, container, move_from, move_to, stack):
# 被移动的箱子
self.value = container
# 箱子移动前的位置
self.move_from = move_from
# 箱子移动后的位置
self.move_to = move_to
# 堆场
self.stack = stack
# 前一个步骤
self.parent = None
# 当前是第几步
self.order = 0
# 修改堆场状态,实际移动箱子
self.stack.move(self.move_from, self.move_to)
# 修改前一个步骤
def set_parent(self, parent):
self.parent = parent
self.order = parent.order + 1
# 启发函数
def _h(self):
length = len(self.stack.get_incorrect_containers())
adjustment = self.value / (self.stack.total_row * self.stack.max_height)
return length - adjustment
# 计算从开始状态到当前实际多少步
def _g(self):
return self.order
# 估值函数
def f(self):
return self._g() + self._h()
# 判断两个步骤是否等价:如果对应的堆场状态完全一样,则两个步骤等价
def __eq__(self, other):
return operator.eq(self.stack, other.stack)
# tostring的输出格式
def __str__(self):
return f'({self.value}: {self.move_from} -> {self.move_to}, {self.order}, {self.f()})'
# 打印时的表现形式
def __repr__(self):
return self.__str__()
class StepList(object):
def __init__(self, values=None):
super().__init__()
self.linked_index = {}
self.sorted_steps = {}
self.min_step = None
self.length = 0
if values is not None:
self.append(values)
def __len__(self) -> int:
return self.length
# Mixin methods
def append(self, value: Step) -> None:
self.__add_item(value)
def remove(self, value: Step) -> None:
self.__del_item(value)
def get_item(self, value: Step):
row = value.stack.total_row
height = value.stack.max_height
current_index = self.linked_index
for i in range(row):
for j in range(height):
if value.stack.containers[i][j] in current_index:
current_index = current_index[value.stack.containers[i][j]]
else:
return None
return current_index['step']
def next_step(self):
return self.min_step
def __add_item(self, value: Step):
row = value.stack.total_row
height = value.stack.max_height
current_index = self.linked_index
for i in range(row):
for j in range(height):
current_index = current_index.setdefault(value.stack.containers[i][j], {})
current_index['step'] = value
self.length += 1
if value.f() not in self.sorted_steps:
self.sorted_steps[value.f()] = []
self.sorted_steps[value.f()].append(value)
if self.min_step is None or value.f() < self.min_step.f():
self.min_step = value
def __del_item(self, value: Step):
row = value.stack.total_row
height = value.stack.max_height
current_index = self.linked_index
for i in range(row):
for j in range(height):
current_index = current_index[value.stack.containers[i][j]]
current_index.clear()
self.length -= 1
steps = self.sorted_steps[value.f()]
steps.remove(value)
if len(steps) == 0:
del self.sorted_steps[value.f()]
if self.min_step == value:
self.min_step = self.sorted_steps[min(self.sorted_steps.keys())][0]
if __name__ == '__main__':
start = datetime.now()
# 最初的堆场状态
current_stack = Stack(origin_stack, S, T)
current_step = None
open_steps = StepList()
close_steps = StepList()
# 如果移动后还是有摆放位置不对的箱子,则继续调整
while len(current_stack.get_incorrect_containers()) > 0:
print(len(open_steps))
# A*算法的处理:注意步骤比较会按照Step类的__eq__方法,即比较步骤对应的堆场状态
available_steps = current_stack.list_available_steps(close_steps)
for step in available_steps:
# 如果步骤在open list中,即open list中有其它步骤形成了与可用步骤相同的堆场状态
open_step = open_steps.get_item(step)
# 与open list中等价步骤的order进行比较,如果原有步骤的order比可用步骤大,则删除原有的步骤, 否则跳过可用步骤
if open_step is not None:
if open_step.order > step.order:
open_steps.remove(open_step)
else:
continue
# 将可用步骤链接到当前步骤之后,然后加入open list
if current_step is not None:
step.set_parent(current_step)
open_steps.append(step)
# 当前步骤被从open list移动到close list
if current_step is not None:
open_steps.remove(current_step)
close_steps.append(current_step)
# 如果已经没有可用的步骤了,输出无法完成,结束程序
if len(open_steps) == 0:
print("It can't be done.")
print(len(close_steps))
exit(0)
# 根据估值函数最小的原则找到移动的下一步
current_step = open_steps.next_step()
current_stack = current_step.stack
# 如果正确退出循环,说明找到了移动方法,倒序打印所有步骤
actual_steps = [current_step]
while current_step.parent is not None:
actual_steps.append(current_step.parent)
current_step = current_step.parent
for step in reversed(actual_steps):
print(step)
step.stack.print()
# 打印步骤数
print(f'It could be done in {len(actual_steps)} steps.')
end = datetime.now()
print("Start Time =", start.strftime("%H:%M:%S"), "End Time =", end.strftime("%H:%M:%S"))
print(len(close_steps))