6-递归与回溯
目录
1 递归典型问题
LeetCode 17-电话号码的字母组合
LeetCode 93-复原IP地址
LeetCode 131-分割回文串
2 回溯法的应用
LeetCode 46-全排列
LeetCode 47-全排列 II
3 组合问题
LeetCode 77-组合
LeetCode 39-组合总和
LeetCode 40-组合总和 II
LeetCode 216-组合总和 III
LeetCode 78-子集
LeetCode 90-子集 II
LeetCode 401
4 在二维平面上使用回溯法
LeetCode 79-单词搜索
LeetCode 200-岛屿数量
LeetCode 130-被围绕的区域
LeetCode 417
1 递归典型问题
递归法不仅仅局限于二叉树这些已经明确的数据结构中的使用,在更广泛的问题中也会使用,这类问题通常有一个明显的特征即树形结构。
例1:LeetCode 17,根据题目可以分析出如下的结构图:从图上看这是一个明显的树形结构。
把图换位形式化的表述:
其代码如下:
class Solution { //保存所对应的的字符串 private String[] letterMap = new String[]{"abc","def","ghi","jkl", "mno","pqrs","tuv","wxyz"}; //用来保存结果 List<String> res = new LinkedList<>(); public List<String> letterCombinations(String digits) { //清空,当为空的时候不应该有任何东西 res.clear(); if (digits.equals("")){ return res; } findCombination(digits,0,""); } //s中保存了从digits[0...index-1]翻译得到的一个字符串 //这个方法是寻找和digits[index]匹配的字母,获得digits[0...index]翻译得到的解 private void findCombination(String digits,int index,String s){ //递归的终止条件 if (index == digits.length()){ res.add(s); return; } char c = digits.charAt(index); String letters = letterMap[c-'2']; for (int i = 0; i < letters.length(); i++) { findCombination(digits,index+1,s+letters.charAt(i)); } return; } }
分析整个代码流程可以看出,递归调用每次都会返回一个结果,根据这个特点递归也常被称为回溯,回溯法也是常用的暴力解决方法。在本题中比较简单,只需要暴力求解即可了,后面类似题目有LeetCode 93、131
2 回溯法的应用
例1:LeetCode 46,根据题目可以分析出如下图结构:同样是一个树形结构
形式化的表述如下:
与前面的题目有所不同的是这里的数字会相互影响的,而在上面的题目中数字与数字之间是不会冲突的。
通过直接暴力递归便可进行回溯
class Solution { public List<List<Integer>> permute(int[] nums) { List<List<Integer>>res = new LinkedList<List<Integer>>(); backTrack(nums,new LinkedList<Integer>(),res); return res; } private void backTrack(int[] nums, LinkedList<Integer> path, List<List<Integer>> res) { if (path.size() == nums.length){ res.add(new LinkedList<Integer>(path)); return; } for (int i = 0; i < nums.length; i++) { //当已经存放过元素时,直接跳过 if (path.contains(nums[i])) continue; //把元素添加进路径中 path.add(nums[i]); backTrack(nums,path,res); //回溯时返回之前状态 path.removeLast(); } } }
这个代码是优化后的,添加了记忆化搜索
/** * 本题和17题的答案很相似,只不过在递归的时候要加入一些限制条件 */ class Solution { //返回的结果 List<List<Integer>> res = new LinkedList<>(); //记录元素状态 boolean[] used; public List<List<Integer>> permute(int[] nums) { //清空 res.clear(); if (nums.length == 0){ return res; } //构造一个数组用来记录元素是否被使用过了 used = new boolean[nums.length]; Arrays.fill(used,false); //用来保存中间的生成结果 ArrayList<Integer> p = new ArrayList<>(); generatePermution(nums,0,p); return res; } // p中保存了一个有index个元素的排列 // 该方法向这个排列的末尾添加第index+1个元素,获得一个有index+1个元素的排列 private void generatePermution(int[] nums,int index, ArrayList<Integer> p){ if (index == nums.length){ //添加的时候必须要加上new不知道为什么 //res.add(p)这样是添加进去没有数据的 res.add(new LinkedList<Integer>(p)); return; } //递归部分 for (int i = 0; i < nums.length; i++) { if (!used[i]){ //如果第i个元素没有被使用,则将第i个元素添加到p中 p.add(nums[i]); //使用过后修改元素状态 used[i] = true; //递归调用方法 generatePermution(nums,index+1,p); /** * 一开始我是想着generatePermution(nums,index+1,p.add(nums[i]));这样调用,但是出错,查询了api后才发现原因 * linkedlist的add方法返回的是boolean类型,没有办法转换为list类型的所以必须先添加然后再删除 */ //当递归调用后需要回去,这时应该把所有的状态恢复的 p.remove(p.size()-1); used[i] = false; } } return; } }
这道题目稍微提高的为47需要处理一下。
//这道题目虽然跑的很慢,但是自己做出来的,仿照46,虽然元素会重复但是下标不会重复,因此保存的是下标,最后结果输出的时候把转成相应的数字 class Solution { public List<List<Integer>> permuteUnique(int[] nums) { Set<List<Integer>> set = new HashSet<List<Integer>>(); backtrack(nums,new LinkedList<Integer>(),set); return new LinkedList<List<Integer>>(set); } private void backtrack(int[] nums, LinkedList<Integer> path, Set<List<Integer>> set) { if (path.size() == nums.length) { LinkedList temp = new LinkedList(); for (int i = 0; i < path.size(); i++) { temp.add(nums[path.get(i)]); } set.add(new LinkedList<Integer>(temp)); return; } for (int i = 0; i < nums.length; i++) { if (path.contains(i)) continue; path.add(i); backtrack(nums,path,set); path.removeLast(); } } }
3 组合问题
例1::LeetCode 77,根据题目可以画出如下的图示:
这道题目也可以按照总结的回溯模板写出代码,虽然很占时间,但是编写的速度快,也能通过
public List<List<Integer>> combine(int n, int k) { List<List<Integer>> res = new LinkedList<List<Integer>>(); backTrack(n,k,1,new LinkedList<Integer>(),res); return res; } private void backTrack(int n,int k,int start, LinkedList<Integer> path, List<List<Integer>> res) { if (path.size() == k){ res.add(new LinkedList<Integer>(path)); return; } for (int i = start; i <= n; i++) { //当已经存放过元素时,直接跳过 if (path.contains(i)) continue; //把元素添加进路径中 path.add(i); backTrack(n,k,i+1,path,res); //回溯时返回之前状态 path.removeLast(); } }
对于组合问题是不考虑数字顺序的,因此分支要少很多,之所以选用递归,是因为每次的操作流程都差不多,都是从一个数组中取出一个数字。在优化的时候还要考虑剪枝操作。其代码如下:
class Solution { //保存结果 private List<List<Integer>> res = new LinkedList<>(); public List<List<Integer>> combine(int n, int k) { res.clear(); if (n <= 0|| k<=0 ||k>n){ //当不满足条件时直接返回即可 return res; } LinkedList<Integer> temp = new LinkedList<>(); //根据题目要求最开始是从1搜索的 generateCombinations(n,k,1,temp); return res; } //求解C(n,k),当前已经找到的组合存储在c中,需要从start开始搜索新的元素 private void generateCombinations(int n,int k, int start, LinkedList<Integer> temp){ //递归的终止条件 if (temp.size() == k){ res.add(new LinkedList<>(temp)); return; } /* 这里是没有进行剪枝优化的操作,可在博客的图中看出不管怎样还会遍历到n=4,但实际上对于这种我们并不想遍历 //在写递归时这里的递归循环可以放在最后写,先把第一次的调用流程写完 for (int i = start; i <= n ; i++) { temp.add(i); generateCombinations(n,k,i+1,temp); //要返回原来的状态,即回溯 temp.remove(temp.size()-1); } */ // 在没有循环前一共是有k-c.seize()个空位的,所有在[i...n]中要有k-c.seize()个元素 // 因此i最多为n-(k-c.seize())+1 for (int i = start; i <= n-(k-temp.size())+1; i++) { temp.add(i); generateCombinations(n,k,i+1,temp); //要返回原来的状态,即回溯 temp.remove(temp.size()-1); } } }
与此类似的题目有39、40、216、78、90、401
代码实现:
LeetCode-39
public List<List<Integer>> combinationSum(int[] candidates, int target) { List<List<Integer>> res = new LinkedList<List<Integer>>(); //Arrays.sort(candidates); //先对无序的数组排序 backtrack(candidates,target,res,0,new LinkedList<>()); return res; } private void backtrack(int[] candidates, int target, List<List<Integer>> res, int start, LinkedList<Integer> temp) { if (target < 0) return; if (target == 0){ res.add(new LinkedList<Integer>(temp)); return; } for (int i = start; i < candidates.length; i++) { if (target < 0) break; temp.add(candidates[i]); //这里与前面不同,不是i+1 backtrack(candidates,target-candidates[i],res,i,temp); temp.removeLast(); } }
LeetCode-40
public List<List<Integer>> combinationSum2(int[] candidates, int target) { Set<List<Integer>> set = new HashSet<>(); //先排序再用set集合,就解决了[1,7][7,1]这样的问题 Arrays.sort(candidates); backtrack(candidates,target,0,new LinkedList<>(),set); return new LinkedList<>(set); } private void backtrack(int[] candidates, int target, int start, LinkedList<Integer> temp, Set<List<Integer>> set) { if (target < 0) return; if (target == 0){ set.add(new LinkedList<>(temp)); return; } for (int i = start; i < candidates.length; i++) { if (target < 0) return; temp.add(candidates[i]); backtrack(candidates,target-candidates[i],i+1,temp,set); temp.removeLast(); } }
4 在二维平面上使用回溯法
例1:LeetCode 79。本题中使用的二维平面不容易直接思考,最好是画出图形来。左侧是给定的字符数组,右侧是待寻找的字符串。
在寻找的时候从(0,0)位置开始寻找,按照上、右、下、左的顺时针顺序进行递归寻找。
class Solution { // 这是定义了四个方向的位移,上,右,下,左 private int[][] d = new int[][]{{-1,0},{0,1},{1,0},{0,-1}}; // 定义数组的范围的变量 private int m,n; // 记录元素是否被访问过 private boolean[][] visited; public boolean exist(char[][] board, String word) { // 二维平面的长度 m = board.length; n = board[0].length; // 初始化全部为false visited = new boolean[m][n]; for (int i = 0; i < m; i++) { for (int j = 0; j < n; j++) { visited[i][j] = false; } } for (int i = 0; i < m; i++) { for (int j = 0; j < n; j++) { if (searchWord(board,word,0,i,j)){ return true; } } } return false; } // 从board[startx][starty]开始,寻找word[index...word.size()-1] private boolean searchWord(char[][] board,String word,int index,int startx,int starty){ // 递归的终止条件,搜寻到最后一个元素时直接判断 if (index == word.length() - 1){ return board[startx][starty] == word.charAt(index); } if (board[startx][starty] == word.charAt(index)){ visited[startx][starty] = true; // 从startx、starty出发,向四个方向寻找 for (int i = 0; i < 4; i++) { int newx = startx + d[i][0]; int newy = starty + d[i][1]; // 判断是否越界,并且元素以前并没有被访问过 if (inArea(newx,newy) && !visited[newx][newy]){ if (searchWord(board,word,index+1,newx,newy)){ return true; } } } visited[startx][starty] = false; } return false; } // 判断给定的坐标是否在二维平面中 private boolean inArea(int x,int y){ return x>= 0 && x< m && y>=0 && y < n; } }
例2:floodfill算法,LeetCode 200。floodfill就是在区域内不断的进行深度优先遍历,进行着色,其代码和例1很类似。
class Solution { // 这是定义了四个方向的位移,上,右,下,左 private int[][] d = new int[][]{{-1,0},{0,1},{1,0},{0,-1}}; // 定义数组的范围的变量 private int m,n; // 记录元素是否被访问过 private boolean[][] visited; public int numIslands(char[][] grid) { // 二维平面的长度 m = grid.length; // 在测试用例中有数据为空,必须判断一下 if (m == 0){ return 0; } n = grid[0].length; // 初始化全部为false visited = new boolean[m][n]; for (int i = 0; i < m; i++) { for (int j = 0; j < n; j++) { visited[i][j] = false; } } int res = 0; // 遍历二维平面 for (int i = 0; i < m; i++) { for (int j = 0; j < n; j++) { if (grid[i][j] == '1' && !visited[i][j]){ res ++; // 进行深度优先遍历,进行标记 dfs(grid,i,j); } } } return res; } // grid[x][y]的位置开始,进行floodfill算法 // 在内部的if三个条件中保证(x,y)合法,其grid[x][y]是没有访问过的陆地 private void dfs(char[][] grid,int x,int y){ // 访问到当前坐标了标记为true visited[x][y] = true; for (int i = 0; i < 4; i++) { int newx = x + d[i][0]; int newy = y + d[i][1]; // 在这个递归中没有定义递归终止条件,其实在这三个判断中已经定义好了递归的终止条件,不满足这三个条件就无法进入递归中 if (inArea(newx,newy) && !visited[newx][newy] && grid[newx][newy] == '1'){ dfs(grid,newx,newy); } } } // 判断给定的坐标是否在二维平面中 private boolean inArea(int x,int y){ return x>= 0 && x< m && y>=0 && y < n; } }
与此类似题目:LeetCode 130、417
5 困难问题视频中有讲但还没记录
递归常常包含着回溯的思想。