您的位置:首页 > 其它

算法框架与问题求解

2013-08-30 17:32 411 查看

算法框架与问题求解

目录

什么是回溯法?

回溯法的通用框架

利用回溯法解决问题

问题1:求一个集合的所有子集

问题2:输出不重复数字的全排列

问题3:求解数独——剪枝的示范

问题4:给定字符串,生成其字母的全排列

问题5:求一个n元集合的k元子集

问题6:电话号码生成字符串

问题7:一摞烙饼的排序

问题8:8皇后问题

总结与探讨

附:《算法设计手册》第7章其余面试题解答

  摘了一段来自百度百科对回溯法思想的描述:


在包含问题的所有解的解空间树中,按照深度优先搜索的策略,从根结点出发深度探索解空间树。当探索到某一结点时,要先判断该结点是否包含问题的解,如果包含,就从该结点出发继续探索下去,如果该结点不包含问题的解,则逐层向其祖先结点回溯。(其实回溯法就是对隐式图的深度优先搜索算法)。 若用回溯法求问题的所有解时,要回溯到根,且根结点的所有可行的子树都要已被搜索遍才结束。 而若使用回溯法求任一个解时,只要搜索到问题的一个解就可以结束。


  可以把回溯法看成是递归调用的一种特殊形式。其实对于一个并非编程新手的人来说,从来没使用过回溯法来解决问题的情况是很少见的,不过往往是“对症下药”,针对特定的问题进行解答。这些天看了《算法设计手册》回溯法相关内容,觉得对回溯法抽象的很好。如果说算法是解决问题步骤的抽象,那么这个回溯法的框架就是对大量回溯法算法的抽象。本文将对这个回溯法框架进行分析,并且用它解决一系列的回溯法问题。文中的回溯法采用递归形式。

  在进一步的抽象之前,先来回顾一下DFS算法。对于一个无向图如下图左,它的从点1开始的DFS过程可能是下图右的情况,其中实线表示搜索时的路径,虚线表示返回时的路径:

construct_candidates()、next_square()、possible_values()
  由于要对定义的数据结构进行修改,make_move()和unmake_move()也需要进行实现了。

make_move()和unmake_move()
  is_a_solution()是对freecount是否为0的判断,process_solution()可以用作输出填好的数独,这两个函数的解法略过。而backtrack()函数和基本框架相比,看上去没多大的区别。

backtrack of sudoku
  经测试,《算法设计手册》上的Hard级别的数独,我的这个程序可以获得和原书同样的解。

问题4解法
显然,construct_candidates()已经化入了backtrace()内部,而且这也是一个对如何将候选也作为参数传递给下一层递归的很好的展示。

问题5:求一个n元集合的k元子集(n>=k>0)。(《算法设计手册》面试题7-15)

解答:

  如果想采用问题1的解法,需要稍作修改,使得遍历至叶结点(也即所有元素都进行标记是否在集合中)时,判断是不是一个解,即元素数目是否为k。满足才能输出。

问题5解法完整示例

问题6:电话号码对应字符串

  电话键盘上有9个数字,其中2~9分别代表了几个字母,如2:ABC,3:DEF......等等。给定一个数字序列,输出它所对应的所有字母序列。(《算法设计手册》面试题7-17,以及《编程之美》3.2“电话号码对应英语单词”)

解答:

  这个问题在回溯法里已经很简单了,因为每一步的选择都不影响下一步的选择。稍微要注意的一点是如何把数字与多个字母的对应关系告诉程序。这个存储结构和相应的construct_candidates()可能是这样的:

烙饼排序
  其实理解这个算法的关键是如何把“翻转烙饼”的过程抽象成数据结构的改变,回溯法倒不是那么重要。

问题8:8皇后问题

  国际象棋棋盘上有8*8个格子。现在有8枚皇后棋子,一个格子只能放一个棋子,求解所有放法,使得这些棋子不同行、不同列、且不在对角线上((4,5)和(5,6)就是在对角线上的情况,不合法)。

解答:

  上面练习了那么多回溯法的问题,我相信能看到这里的人水平已经足以解决这个问题了。按行放置可以保证棋子不同行,对于每种放置可能,检查是否与上面各行的棋子是否同列、同对角线。都不满足的才能选作此次的决策即可。

8皇后问题

总结与探讨

  通过以上的实例,可以发现回溯法框架确实能够解决许多形态各异的问题,这也得归功于这个框架足够抽象而不限于具体问题的求解,其通用性毋庸置疑。

  然而如果一个问题看到之后就有了思路,并能直接写出类似于问题2的精简版的情况又如何呢?这种情况下当然就没必要再去套用回溯法框架了,因为你已经把这个框架的步骤内化到自己的思考中并能在这个问题上运用自如了,这一点是值得高兴的。这时回溯法框架对于你来说只是用于检查代码正确性的一种额外验证方式罢了,没必要退而求其次。

  当你思路比较混乱,不知如何下手时我才建议搬出回溯法框架进行分析和套用。不过从问题7烙饼排序中可以看到,有时思路的不清晰往往是对实际问题的抽象不够,而不是编写回溯法解决本身的问题。

  编写回溯法时应该注意尽可能剪枝,同时维护好构造候选时所用的数据结构。

附:《算法设计手册》第7章其余面试题解答

7-16.

  请用给定字符串中的字母重新组合成在字典中的单词。比如Steven Skiena可以重组为Vainest Knees。

解答:

  虽然通过回溯法可以把所有情况列出并与字典对照,但这未免太没有效率了。

  更快的方法是把给定字符串和所有字典单词排序成字母序,比如apple变成aelpp,再对排序后的字符串在排序后的字典进行搜索。这是个变位词的变形,变位词的处理可以参考:http://www.cnblogs.com/wuyuegb2312/p/3139926.html#title21

7-18.

  一间能容纳n个人的空房,房外有n个人。你站在门口,可以选择让门外的一个人进屋,也可以选择让屋内的人出来一个。请输出所有的2n种屋中人的出现情况的可能,并且这些情况是相邻的(上一种情况通过一次操作能变成下一种情况)

解答:

  一开始不是很理解,参考答案上也提到是用格雷码来解决。不过如果知道格雷码的生成方式,就好解决了:



1位格雷码有两个码字

(n+1)位格雷码中的前2n个码字等于n位格雷码的码字,按顺序书写,加前缀0

(n+1)位格雷码中的前2n个码字等于n位格雷码的码字,按逆序书写,加前缀1



  不过为了输出美观,由于C语言不提供printf直接输出2进制数,需要把10进制数转化成2进制数输出,而且首端的0要补上,这需要花点心思。下面是一个生成4位格雷码的程序,并不是回溯法。(为了省事直接在回溯法框架上改的)

格雷码生成

7-19.

  使用能生成随机数{0,1,2,3,4}的函数rng04来生成rng07。每次运行rng07平均要调用几次rng04?

解答:

  随机数生成函数以前已经分析过了:/article/4935242.html。对于调用次数的期望,

  如果将rng07写作rng03+4*rng01,那么rng04调用的次数为它在rng03和rng01之和,都是
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: