A*算法解决翻箱问题 ---- 性能优化

这篇是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))

  • 3
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

blkq

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值