使用A星算法解决过桥问题

上周朋友提到一个智力题:

有4个人在晚上通过一座摇摇欲坠的小桥,并且每次只能让2个人过去。过桥的人必须要用到手电筒,不然会出事故,只有一个手电筒,4个人的行走速度不同。亚当用1分钟就可以走完,拉里用2分钟。埃奇用5分钟,最慢的汤姆用10分钟。两人同行时, 过桥速度取决于速度较慢者。请问用什么方法才能最快过桥,用时多少。

人肉一番:如果已经有人过桥,那么回送手电的必须是已经过桥的人中速度最快的那一个,而要想桥对面有过桥快的人,必须有过桥快的人过去。人就4个,比较好设计,答案可在1分钟内思考得到:

  1. 亚当和拉里先过,用时2分钟,然后亚当拿着手电筒再回来,用时3分钟。
  2. 汤姆和埃奇过用时10分钟,然后拉里拿着手电筒再回来,用时12分钟。
  3. 最后是亚当和拉里再回来,用时2分钟,总用时是17分钟。

后来提到如果人多了,问题规模变大了,怎么办呢?想必大家都知道,那就是交给机器去解决.

召唤AI

人工智能领域对于此类问题是有匹配的解决方案的,即A*算法.
它是一种在有多个节点的图中寻找达到目标节点最低成本的算法. 该算法综合了BFS和Dijkstra最短路径算法的优点, 主要是通过启发式搜索提高算法效率. 如果以g(n)表示从起点到任意顶点n的实际距离,h(n)表示任意顶点n到目标顶点的估算距离(根据所采用的评估函数的不同而变化),那么 A*算法的估算函数为:

  • 如果g(n)为0,即只计算任意顶点n到目标的评估函数h(n),而不计算起点到顶点n的距离,则算法转化为BFS(Best First Search),此时使用的是贪心策略,速度最快,但可能得不出最优解;
  • 如果h(n)不为0,则一定可以求出最优解,而且h(n)越小,需要计算的节点越多,算法效率越低,常见的评估函数有——欧几里得距离、曼哈顿距离、切比雪夫距离;
  • 如果h(n)为0,即只需求出起点到任意顶点n的最短路径g(n),而不计算任何评估函数h(n),则转化为单源最短路径问题,即Dijkstra算法,此时需要计算最多的定点;

关于A*算法的更多性质和证明可参考wiki(比如一个有效评估f(n)必须要小于实际距离算法才有效的证明).本文就不再详细介绍.我们主要看一下当前这个问题,如何使用A-Star算法解决.

评估函数

虽然理论有了, 但具体问题还要具体分析. 正如前文所述,不同的问题有不同的评估函数,在建立评估函数前,需要对这个问题进行建模.

问题建模

状态

每当有人过桥,相对于初始的场景,都会生成一个新的场景. 对应桥的两端,可以使用tuple来表示. (1,2,5,10)<->() 就对应最开始的状态(假设左侧是未过桥,右侧tuple是已经过桥的). 比如汤姆(5)和埃奇(10)过桥,那么状态变为(1,2)<->(5,10).

状态迁移

还有一个问题,就是当前的状态下一步到底是有人从左到右还是从右到左呢. 这时,我们需要把手电筒加入到状态中:(1,2)<->(5,10), 'r', 如果最后一个标志位是r,那么下一步的操作就是亚当(1)和拉里(2)过桥. 可以使用python的namedtuple来表示如下:
Status = namedtuple('Status', ['left','right','torch'])

评估函数设计

有了这个模型, 问题变得比较清晰. 对于g(n)很简单,就是已经耗时多少, 那么h(n)就是从任一状态到()<->(1,2,5,10)的耗时函数了.设计h(n)最关键的地方在于,一定不要overestimate即可.

  • 假设下一步是过河,且还有3人以上未过河, h(n)取左侧最大耗时加最小耗时
  • 假设下一步是过河,还有2人或少于未过河, h(n)取左侧最大耗时
  • 假设下一步是回送手电, 则h(n) 为 右侧最小耗时加左侧的h(n)

公式如下:



有了h(n)的公式,那么只需要把BFS算法加上priority-queue, 就可得到最终的A-Star算法了.

代码实现

代码如下:

from itertools import combinations
import heapq as hq

def tuple_eq(self, other):
    if len(self) != len(other):
        return False
    for e in self:
        if e not in other:
            return False
    return True


class Status(object):
    def __init__(self, left, right, torch, prev = None):
        self.left = left
        self.right = right
        self.torch = torch
        self.prev = prev

    def __eq__(self, other):
        return self.torch == other.torch and tuple_eq(self.left, other.left) and tuple_eq(self.right, other.right)

    def estimate(self):
        def estimate_left():
            return max(self.left) if len(self.left) <= 2 else max(self.left) + min(self.left)
        if self.torch == 'l':
            return estimate_left()
        elif self.left:
            return min(self.right) + estimate_left()
        return 0

    def yield_next(self):
        def tuple_sub(minuend, subtrahend):
            return tuple(set(minuend) - set(subtrahend))
        def tuple_add(t1, t2):
            return tuple(set(t1).union(set(t2)))
        if self.torch == 'l':
            for e in combinations(self.left, 2):
               yield max(e), Status(tuple_sub(self.left, e),tuple_add(self.right, e), 'r')
        if self.torch == 'r':
            yield min(self.right), Status(tuple_add(self.left, (min(self.right),)),
                    tuple_sub(self.right, (min(self.right),)), 'l')

    def __str__(self):
        return '[' + str(self.left) + '--' + str(self.right) + ']'

    def reverse_path(self):
        if not self.prev:
            return str(self)
        return self.prev.reverse_path() + '->' + str(self)

start = Status((1,2,5,10), (),'l')
goal = Status((), (1,2,5,10),'r')
BIG_NUM = 1000
def A_Star(start, goal):
    nexts = [(BIG_NUM,0,start)]
    closed_set = set()

    while nexts:
       f_score, already_cost, current_status = hq.heappop(nexts)
       if current_status == goal:
           return already_cost, current_status.reverse_path()
       closed_set.add(current_status)
       for cost, next_status in current_status.yield_next():
           if next_status not in closed_set:
               next_status.prev = current_status
               closed_set.add(next_status)
               hq.heappush(nexts, (already_cost + cost + next_status.estimate(), already_cost + cost, next_status))
    return 'fail'

print A_Star(start,goal)

实现策略方面:

  • 放弃使用namedtuple自动生成类的原因是还需要添加其他函数, 每次都Status.estimate = estimate的方式很不美观.
  • 为了简化代码,认为所有同学的过桥速度均不相等(这样可以使用内置的set表示).

代码运行结果:
(17, '[(1, 2, 5, 10)--()]->[(10, 5)--(1, 2)]->[(1, 10, 5)--(2,)]->[(1,)--(2, 10, 5)]->[(1, 2)--(10, 5)]->[()--(1, 10, 2, 5)]')

你可能感兴趣的:(使用A星算法解决过桥问题)