一文学会回溯算法解题技巧中对回溯法的描述很通俗易懂,现将基本概念迁移到此。
深度优先算法用到了回溯的算法思想,这个算法虽然相对比较简单,但很重要,在生产上广泛用在正则表达式,编译原理的语法分析等地方,很多经典的面试题也可以用回溯算法来解决,如八皇后问题,排列组合问题,0-1背包问题,数独问题等,也是一种非常重要的算法。
什么是回溯算法
回溯算法本质其实就是枚举,在给定的枚举集合中,不断从其中尝试搜索找到问题的解,如果在搜索过程中发现不满足求解条件 ,则「回溯」返回,尝试其它路径继续搜索解决,这种走不通就回退再尝试其它路径的方法就是回溯法,许多复杂的,规模较大的问题都可以使用回溯法,所以回溯法有「通用解题方法」的美称。
回溯算法解题通用套路
为了有规律地求解问题,我们把问题分成多个阶段,每个阶段都有多个解,随机选择一个解,进入下一个阶段,下一个阶段也随机选择一个解,再进入下一个阶段...
每个阶段选中的解都放入一个 「已选解集合」 中,并且要判断 「已选解集合」是否满足问题的条件(base case),有两种情况
- 如果「已选解集合」满足问题的条件,则将 「已选解集合」放入「结果集」中,并且「回溯」换个解再遍历。
- 如果不满足,则「回溯」换个解再遍历
根据以上描述不难得出回溯算法的通用解决套路伪代码如下:
function backtrace(已选解集合,每个阶段可选解) { if (已选解集合满足条件) { 结果集.add(已选解集合); return; } // 遍历每个阶段的可选解集合 for (可选解 in 每个阶段的可选解) { // 选择此阶段其中一个解,将其加入到已选解集合中 已选解集合.add(可选解) // 进入下一个阶段 backtrace(已选解集合,下个阶段可选的空间解) // 「回溯」换个解再遍历 已选解集合.remove(可选解) } }
通过以上分析我们不难发现回溯算法本质上就是深度优先遍历,它一般解决的是树形问题(问题分解成多个阶段,每个阶段有多个解,这样就构成了一颗树),所以判断问题是否可以用回溯算法的关键在于它是否可以转成一个树形问题。
另外我们也发现如果能缩小每个阶段的可选解,就能让问题的搜索规模都缩小,这种就叫「剪枝」,通过剪枝能有效地降低整个问题的搜索复杂度!
综上,我们可以得出回溯算法的基本套路如下:
- 将问题分成多个阶段,每个阶段都有多个不同的解,这样就将问题转化成了树形问题,这一步是问题的关键!如果能将问题转成树形问题,其实就成功了一半,需要注意的是树形问题要明确终止条件,这样可以在 DFS 的过程中及时终止遍历,达到剪枝的效果
- 套用上述回溯算法的解题模板,进行深度优先遍历,直到找到问题的解。
回溯算法实现三数之和
public class ThreeNumSum1 { public static void main(String[] args) { // TODO Auto-generated method stub ThreeNumSum1 t = new ThreeNumSum1(); int[] nums= {5,-11,-7,-2,4,9,4,4,-5,12,12,-14,-5,3,-3,-2,-6,3,3,-9}; t.threeSum(nums); for(Listl : t.res) { System.out.print(l.get(0) + "\t"); System.out.print(l.get(1) + "\t"); System.out.print(l.get(2)); System.out.println(); } } List > res = new ArrayList<>(); List
selected = new ArrayList<>();//记录索引值 public List > threeSum(int[] nums) { backtrace(nums); return res; } private void backtrace(int[] nums){ if(selected.size() == 3){ if((nums[selected.get(0)] + nums[selected.get(1)] + nums[selected.get(2)]) == 0) { List
tmp = new ArrayList<>(); tmp.add(nums[selected.get(0)]); tmp.add(nums[selected.get(1)]); tmp.add(nums[selected.get(2)]); Collections.sort(tmp); if(!res.contains(tmp)){ res.add(tmp); } } return; } for(int i = 0; i < nums.length; i++){ if(selected.contains(i)) continue; selected.add(i); backtrace(nums); selected.remove(selected.size()-1); } } }
输出:
-3 -2 5 -9 4 5 -14 5 9 -7 -2 9 -7 3 4 -7 -5 12 -2 -2 4 -6 -3 9 -9 -3 12 -6 3 3