算法第五章作业

算法第五章作业

组员:高珞洋,何汶珊

对回溯法的理解

用一句话来说明回溯法给我的印象,那就是它是个很好理解,上手很难,但熟悉后很又很简单的算法。

一开始接触回溯法,有很多例题,比如0-1背包,比如n后问题。给我留下印象最深的还是数独问题,虽然数独问题算是比较复杂的回溯法,但是用它来理解回溯法却是十分形象的。
回溯法实际上基于暴力穷举,当此时的情况不满足某些条件的时候,就放弃当前情况及其后续的情况,换下一种情况。
具体落实到数独问题中,就是遍历这个二维数组(9*9的表格),给每一个没有填的格子填上一个数,当然要把0~9都填一遍,然后填下一个空格。如果当前九宫格内,或者此行、此列已经有这个数了(不满足某些条件),那么就不填这个数(放弃当前情况及其后续的情况)。

但是回溯法是优于暴力穷举的,事实上,回溯法更像是“有脑穷举”。当我们把“判断是否满足某些条件”这个步骤放到“选择当前情况”这个步骤前,即在做选择前先判断,先过脑子,就能很大程度上避免许多无意义的穷举。就像我们知道当前九宫格已经有1了,那么当前空格填1,之后的空格填09,之后的之后的空格填09......等很多情况都可以被忽略,因为不满足条件,所以必不会得出我们要的结果。就可以直接考虑当前空格填2的情况了。

既然“判断是否满足某些条件”这个步骤是回溯法区别于暴力穷举的一个特点,我们将它拿出来,称之为约束函数
而既然需要回溯,那么就需要记录上一个情况的状态(数据),一个非常简单的做法,也是我们通常采用的做法,是利用递归,递归栈的特点能够很好地帮我们保存上一个情况的状态(数据)。而从数独问题我们可以看到,我们选择了一个空格后,进而考虑的是下一个空格,而不是当前空格的下一个情况,即利用的是深度优先搜索而不是广度优先搜索。加之深搜通常也有递归调用,因此回溯法通常采用深搜,既能穷举,又有递归。

说到深搜,我们当然想到了树的结构。实际上,利用回溯法解题,我们总是能将题目看成一个树结构,我们称之为解空间树。求解的过程就是对这棵树进行深度优先搜索,每到一个叶子结点,就是一个解,我们习惯遍历整棵树,找到最优的那个解。
对于回溯法求解的问题,通常可以分为三类。

  1. 第一类是从一个大集合中,选出符合条件的最优解,比如0-1背包问题、子集和问题。可以发现在这类题目中,并非所有的元素都需要选择。因此,对于每个元素,我们都有选和不选两种可能,因此其解空间树是一颗二叉树,又称为子集树。每层表示一个元素,左子树和右子树分别表示选或者不选当前元素(当然也可以反过来)。
  2. 第二类则是对于每个元素,我们必须要给每个元素分配一种情况,当然一个元素可选的情况通常有很多种。就比如数独问题,对于每个元素(空格),我们都有0~9十种选项可供分配,我们仍用元素代表每一层,俺么解空间树自然是一颗10叉树。自然,在这类问题下,其解空间树是一颗n叉树,也称为排列数
  3. 第三类则属于其他类,既不是子集树也不是排列数,比如拆分整数,需要具体问题具体分析。

在更复杂的题目,或者说对时间要求更高的题目中,需要进行剪枝来去除不可能的情况。剪枝是回溯法的精髓所在,也是我认为的难点所在。
剪枝通常包括界限函数约束函数。界限函数将忽略得不到最优解的子树,约束函数则剪去不满足约束条件的子树(跳过不满足某些条件的情况)
通过剪枝能避免大量无效搜索,从而提高算法效率,这也是回溯能优于穷举的根源所在。
比如在0-1背包问题中,我们在选择某项物品前先判断选择该物品会不会使背包超重,超重就不选,这属于约束函数,约束条件就是背包的载重。同时,我们将目前背包剩余重量和剩下没选好的物品进行贪心求解,最后加上目前背包里的价值,即得出当前情况下能得到的最多价值(当前价值+未来能得到的最多价值 [靠贪心求得] );然后将这个值和当前的最优解进行比较,当且仅当未来最多价值比最优解大的时候我们才继续向下遍历这颗子树。
道理很简单:既然未来最多价值比之前我求得的某个最优解小,那么即使我有解(到达叶子结点),也肯定不是最优解,那就没有继续遍历这棵树的必要了。这就属于界限函数

总而言之,回溯法就是通过对解空间的遍历,在解空间不断寻找可行解并更新最优解,最后找到最优的结果。
回溯法最主要的是三点:

  1. 一是构造问题的解空间。不同问题的解空间不同,同一个问题也可以构造不同的解空间,最适合的解空间是便通过于搜索找到答案的一种。因此正确对问题进行上面的三种分类是很好的做法。
  2. 二是搜索策略。回溯法用的是深度优先搜索策略,既能满足遍历,又能实现递归(保存状态)
  3. 三是剪枝。剪枝是回溯法的精髓所在,通过剪枝去掉大量不必要的搜索可以大幅的提高算法的效率。实际上一个问题能有不同的剪枝方法,而不同的方法剪枝的效率是不同的,因此回溯法构造合理的剪枝方式可以决定算法的优劣,十分重要

说明“子集和”问题的解空间结构和约束函数

设集合S={x1,x2,…,xn}是一个正整数集合,c是一个正整数,子集和问题判定是否存在S的一个子集S1,使S1中的元素之和为c。试设计一个解子集和问题的回溯法。

解空间结构

“从一堆数中选择一些满足条件的数”,很显然其解空间是一个子集树。
对于每一个集合中的数,都有选、不选两个选项。因此以每个数作为子集树的一层,选、不选两个选项作为左右两个子树,能够造出一个n层的二叉树。
每一层左右子树分别表示选、不选该数,即,第n层左右子树表示选xn、不选xn。假定左子树是选该数,右子树是不选该数

约束函数

约束函数自然是判断选取当前这个数后是否大于我们需要的和。
如果当前和加上该数大于要求的和,就不选这个数,进入右子树并继续向下遍历。
如果当前和加上该数小于等于要求的和,则将这个数加到当前和中,即选择该书,进入左子树,继续进行遍历。
设当前和为sum,,要求的和为target,即比较sum + x[t] 和 target

界限函数

最简单的界限函数,即判断当前和加上所有剩下没选的数是否大于目标和。
和0-1背包界限函数想法类似,如果当前和加上所有剩下没选的数大于等于目标和,则说明可能有解,继续遍历该树
如果当前和加上所有剩下没选的数小于目标和,则放弃遍历该子树——既然所有数加起来都比目标和小,那么该子树中必不存在解。

结对编程情况

在结对编程中,敲代码的我对于回溯法的掌握略高一筹,因此题目解的比较快。
但是回溯法涉及递归,比较抽象,容易把人绕晕。因此和珊爷讲解讨论代码的时间较长,很多时候需要一步步来,用一个小栗子将递归的过程给一步步呈现。
虽然花的时间比较多,但是能很好的理清两人的思路,从而明白理解递归的过程,而不是单纯的套模板。
在提交过程中,经常出现超时的现象,究其原因是约束函数不够优秀。因此和珊爷讨论探寻更高效的约束函数,在下受益良多。

posted @ 2019-12-17 12:00  BlackDn  阅读(337)  评论(3编辑  收藏  举报
https://blog-static.cnblogs.com/files/luoyang0515/live2d.js https://files.cnblogs.com/files/kousak/waifu.css
Live2D