计算机算法设计与分析(王晓东著) 6-10世界名画陈列馆问题 复盘 附代码(python实现)

6-10 复盘 世界名画陈列馆问题

问题描述懒得写了

分支限界法

  • 分支限界法实际上其实是一种剪枝方法,大体上的框架其实还是属于回溯法。也就是说,是从最初始的节点,进行状态的展开。在状态的展开中,最重要的其实一共只有三点,一点是如何向下延伸,第二点是如何在走不下去的情况下返回上一层的状态,第三点就是每个节点需要保存什么状态。在设计算法的过程中,要始终注意这三点。
  • 在这三点之前,其实非常重要的是,某个特定的算法任务如何通过回溯法的框架来解决。比如本例中,刚开始的时候,怎么分支其实成为了我思考的一个重点。参考了资料之后知道了整体思想是,尽可能覆盖所有为被监视到的点,因此也就要找到未被监视到的点,然后在其周围插一个眼。复盘下来其实感觉要从最后的结束状态出发。本例中结束状态就是所有都被监视了,所以就要一个被监视一个被监视的状态来处理。
  • 说回三点,在上述这三点中,最重要最首先设置的应该是每个节点需要保存什么状态,比如本例中,由于我们确定了每个节点都是未被监视的节点,因此我们需要保存一个哪些节点被监视,哪些节点不被监视的数组。最后需要输出的是眼的位置,因此也需要一个数组展示哪些是眼的位置,哪些不是眼的位置。这两个是核心的棋盘,为了维护这两个棋盘,我们还需要一些拿出来的变量来方便直接看到一些棋盘的特征。当然这些特征是要真正用到的。
  • 接下来是如何向下延伸。根据前面的分析,向下展开其实就是向下插眼,最后求的其实也是眼的数量最小,因此,眼的数量是一个需要的变量。插完眼之后自动的监视的棋盘自然而然也要跟着变化。
  • 最复杂的是回溯。回溯里面可能有很多是没有考虑到的,这部分也是debug上花费时间最长的。在本题中,我采取的是从头到尾一个node的方法,也就是说,在延展下去之后,node还要能够回复到之前的状态。在本例中,就是,我要把自己之前插的眼拔掉。最开始我想的是记录上一个就可以了,每一个记录上一个自然而然就能回到最顶层了。但是这只有每个节点互相都是独立的情况下才成立。而在我的设计中,node是动的。这就给我带来了很大的问题。我的解决方案是,维护了一个栈,这个栈记录了从顶到现在所有我的插眼的坐标,因为栈的FILO性质,所以我可以很方便地pop掉最后一个进来的坐标,从而能够知道我的祖先都是谁。

debug中发现的问题

  • 在debug中还遇到一个问题,就是在寻找下一个未被监视的点。比如,一个棋盘:

    1x1
    010
  • 现在找肯定是找(1, 0)位置作为未被监视的点,那么如果我把眼插在该点的右边,即(1, 1)位置,结果如图:

    1x1
    1x1
  • 盖满了,但是如果我回溯回去的话,情况就又变成了:

    1x1
    010
  • 但是我的算法中,如果遇到到了已经全部被监视的情况,我需要退回去,即在上一步的棋盘中从最后向前找第一个未被监视的格子。此时选择了(1, 2)。也就是说,我可能会空过(1, 0)这个格子,再次寻找下一个的时候可能找不到它。为了解决这个问题,我让向后搜索的函数中加入了一个把搜索头改到最前面再次进行搜索一遍,如果再找不到再说找不到。(更好的解决方案其实是直接不管现在为止是哪,直接从头搜,我这其实是打补丁,下次一定下次一定)

  • 刚开始写算法的时候没想清楚,其实所有的return前面都必须要跟上一个restore的,这里debug了一阵。

  • 还有一个就是,一切都没有问题,到最后一步,返回主函数调用的递归函数的时候,同样需要restore,但是此时因为其实那个眼的栈里面啥都没有了,所以在restore里面的pop会出错,所以给node的初始化里面加了一个(None, None)作为结束标志,也就是说如果发现pop出来的是两个None的话,就不做下面的东西了。

总结

  • 回溯法的三个重要的点,节点内容,扩展,回溯。
  • 描述树结构时候需要考虑的两个点:是移动的还是每个节点分开。如果是移动的,就需要仔细考虑恢复,至少要考虑两层恢复以上,看看需要保存下来哪些数据,每个节点分开则需要每一次下传前要复制一份原来的节点。
  • 在回溯之前是否要一定恢复,仔细考虑好

全部代码

import numpy as np
offsets = [(0, 0), (0, -1), (0, 1), (1, 0), (-1, 0)]
class Node:
    def __init__(self, m, n):
        self.m = m
        self.n = n
        self.grd_board = np.zeros((m, n))
        self.rbt_board = np.zeros((m, n))
        self.grd_num = 0
        self.rbt_num = 0
        self.rbt_array = [(None, None)]
        self.x = 0
        self.y = 0
        self.new_x = None
        self.new_y = None

    def _gen_grd_coor(self, x, y):
        coor = []
        for offset in offsets:
            if x + offset[0] < self.m and x + offset[0] >= 0 and y + offset[1] < self.n and y + offset[1] >= 0:
                coor.append((x + offset[0], y + offset[1]))
        return coor

    def setting(self, x, y):
        # 获取监护地址
        coor = self._gen_grd_coor(x, y)
        # 设置grd
        for c in coor:
            self.grd_board[c[0]][c[1]] += 1
        self.grd_num = (self.grd_board > 0).sum()
        # 设置rbt
        self.rbt_board[x][y] = 1
        self.rbt_num = (self.rbt_board > 0).sum()
        self.rbt_array.append((x, y))
        # 记录新机器人位置
        self.new_x = x
        self.new_y = y

    def restoring(self):
        # 获取原来设置的机器人地址
        new_x, new_y = self.rbt_array.pop()
        # 设置最后结束标志
        if new_x == None and new_y == None:
            return
        coor = self._gen_grd_coor(new_x, new_y)
        # 设置grd
        for c in coor:
            self.grd_board[c[0]][c[1]] -= 1
        self.grd_num = (self.grd_board > 0).sum()
        # 设置rbt
        self.rbt_board[new_x][new_y] = 0
        self.rbt_num = (self.rbt_board > 0).sum()
        # 新机器人位置置空
        self.new_x = None
        self.new_y = None
        # 获得最近的一个无监视的位置供下一次使用
        self.get_prev()

    def _gen_prev(self, x, y):
        y -= 1
        if y < 0:
            y += self.n
            x -= 1
            if x < 0:
                return None, None
        return x, y

    def get_prev(self):
        prev_x, prev_y = self.x, self.y
        if prev_x == None or prev_y == None:
            prev_x, prev_y = self.m - 1, self.n - 1
        while self.grd_board[prev_x][prev_y]:
            prev_x, prev_y = self._gen_prev(prev_x, prev_y)
            if prev_x == None or prev_y == None:
                self.x = prev_x
                self.y = prev_y
                return None
        self.x = prev_x
        self.y = prev_y
        return prev_x, prev_y

    def _gen_next(self, x, y):
        y += 1
        if y >= self.n:
            y -= self.n
            x += 1
            if x >= self.m:
                return None, None
        return x, y

    def get_next(self):
        # 从头搜索标记
        flag = True
        # 获取下一个位置
        next_x, next_y = self.x, self.y
        # 判断下一个位置是否是受监护的
        while self.grd_board[next_x][next_y]:
            next_x, next_y = self._gen_next(next_x, next_y)
            if flag and (next_x == None or next_y == None):
                next_x, next_y = 0, 0
                flag = False
            elif next_x == None or next_y == None:
                self.x, self.y = None, None
                return None
        self.x = next_x
        self.y = next_y
        return (next_x, next_y)

    def to_best(self):
        return self.rbt_num, self.rbt_board.copy()

n = Node(4, 4)
n.setting(0, 2)
print(n.grd_board)
print(n.rbt_board)
print(n.get_next())
n.restoring()
print(n.grd_board)
print(n.rbt_board)
print(n.to_best())

best = [1e7, None]

def build(node):
    global best
    temp = node.to_best()
    if temp[0] > best[0]:
        node.restoring()
        return
    if node.grd_num >= node.m*node.n:
        temp = node.to_best()
        if temp[0] < best[0]:
            best = temp
            node.restoring()
            return
        else:
            node.restoring()
            return
    elif node.grd_board[node.x][node.y]:
        node.get_next()
        build(node)
    else:
        coor = node._gen_grd_coor(node.x, node.y)
        for c in coor:
            node.setting(c[0], c[1])
            node.get_next()
            build(node)
    node.restoring()
    return

if __name__ == "__main__":
    node = Node(4, 4)
    build(node)
    print(best)

  • 3
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值