《Python科学计算》的作者
分类: Python/Ruby
2012-02-25 20:40:52
谜题如下图所示,每个数字N表示一个矩形区域R, R所包含的小方格数正好为N。要求计算出所有数字对应的矩形区域,使得它们之间不互相覆盖,并且正好包含所有的方块。
解决这种问题的最简单的办法就是回溯法,按照顺序把每个数字对应的矩形区域计算出来,如果当前计算出来的矩形区域和已经放置的矩形重叠的话,就尝试下一个候选矩形,直到全部区域被填满。题中一共有22个数字,每个数字对应的可能的矩形区域平均也有5、6种,这样一来,回溯法要花很长时间才能找到答案。
本文给出一种快速算法:传播与分配算法,关于此算法的基本知识,请参照文章:
完整程序
首先我们需要得到所有的数字所对应的所有可能矩形,我们称之为解空间,通过传播与分配算法,逐渐缩小这个解空间的取值范围,直到找到最终解。
解空间我们用英文status来表示,下面的程序计算出问题的初始解空间。
blocks列表是一个3个元素的的元组,(横坐标、纵坐标、此坐标上的数字)。
possible_block2()计算出所有包含坐标(x,y),长宽为(w,h)的矩形区域。它返回的是4个元素的元组,其意义分别为(矩形区域的左上点横坐标, 矩形区域的左上点纵坐标, 矩形区域的宽, 矩形区域的高)。
possible_block()计算出坐标为(x,y)的数字c所对应的所有可能的矩形区域。例如:
make_init_status()通过收集所有的数字对应的所有可能的矩形区域,返回本问题的初始解空间status。status[i]为blocks[i]对应的所有可能的解。
传播和分配的基本思想是通过传播操作,尽量缩小解空间的取值范围,在无法缩小的情况下,进行解空间的分配操作,也就是把解空间分解成两个子空间,然后分别对其进行传播和分配操作,直到找到解为止。
我们先来看看传播操作。对于本问题,传播操作就是剔除掉所有相互重叠的矩形。具体地说,就是当某个矩形R与某个数字N对应的所有的矩形都重叠的时候,从解空间中删除矩形R,下文称这种矩形为无效矩形。下面是这部分代码的说明。
rectangle_overlap()判断矩形rect1和矩形rect2是否有重叠部分,有则返回True,无则返回False。
rectangle_overlap_all()判断矩形rect和矩形列表rectlist中的所有矩形是否都重叠,都重叠时返回True。
remove_overlap_rectangles()从矩形列表rectlist1中删除掉与矩形列表rectlist2中所有矩形重叠的矩形,如果删除了某个矩形的话,返回True,否则返回False。
del_impossible_choice()完成对解空间status的传播操作,从解空间status中删除掉所有无效矩形,直到找不到更多的无效矩形。tocheck是一个待检查的集合,它的元素为一个2元元组(x, y),表示要删除status[x]中所有与status[y]重叠的矩形。当remove_overlap_rectangles()返回Ture时,表示status[x]发生了变化,因此需要重新检查(i, x),i为除了x之外的所有下标。当tocheck为空时,则表示无法继续进行传播操作了。此时的解空间的大小将明显的减少。例如本问题的初始解空间中共有个345矩形元素,经过一次传播操作之后剩下156个矩形元素。
当传播操作无法继续时,进行分配操作,通过分配算法将当前的解空间分解为两个子解空间,然后再递归执行传播分配操作。下面是这部分程序的解释。
divide_status()对解空间status进行分配,得到两个子解空间status1和status2。分配的算法有很多种,这里采用的具体算法如下:
程序中的status1和status2是分解后的子解空间,显然status1和status2的集合就是status。
is_solved_status()判断status是否是最终解。当解空间status中所有的候选矩形数都为1时,则找到了最终解。
is_impossible_status()判断status是否是无解解空间。 当解空间status中某组候选矩形数为0时,表示此空间无解。
solve_puzzle()递归调用自己来实现传播分配操作,直到找到解。
最后我们来运行一下程序:
print_solve_status()将解打印成易读的形式:
最终的输出结果为:
在我的电脑上整个计算过程不过2,3秒。我用回溯法解了一下这个题,睡了一觉才等到答案,所以不知道到底花了多长时间。:-)