回溯算法 常见的使用方法逻辑整理
1. 回溯算法 特点
回溯算法实际上一个类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就“回溯”返回,尝试别的路径。回溯法是一种选优搜索法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。
2. 回溯算法 通用方法
用回溯算法解决问题的一般步骤:
- 针对所给问题,定义问题的解空间,它至少包含问题的一个(最优)解。
- 确定易于搜索的解空间结构,使得能用回溯法方便地搜索整个解空间 。
- 以深度优先的方式搜索解空间,并且在搜索过程中用剪枝函数避免无效搜索。
回溯是经过修改的深度优先查找方法,过程包括:对一个状态空间树进行深度优先查找,检查每个节点是否满足条件。如果不满足就回溯到该节点的父节点。算法框架(伪代码)如下:
result = []
backtrack(路径, 选择列表):if 满足结束条件:result.add(路径)returnfor 选择 in 选择列表:做选择backtrack(路径, 选择列表)撤销选择
3. 常见面试题
一些总结
3.1 单词搜索
给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false 。
单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。
示例 1:
输入:board = [[“A”,“B”,“C”,“E”],[“S”,“F”,“C”,“S”],[“A”,“D”,“E”,“E”]], word = “ABCCED”
输出:true
示例 2:
输入:board = [[“A”,“B”,“C”,“E”],[“S”,“F”,“C”,“S”],[“A”,“D”,“E”,“E”]], word = “SEE”
输出:true
示例 3:
输入:board = [[“A”,“B”,“C”,“E”],[“S”,“F”,“C”,“S”],[“A”,“D”,“E”,“E”]], word = “ABCB”
输出:false
解法
-
递归参数: 当前元素在矩阵 board 中的行列索引 i 和 j ,当前目标字符在 word 中的索引 k 。
-
终止条件:
- 返回 false : (1) 行或列索引越界 或 (2) 当前矩阵元素与目标字符不同 或 (3) 当前矩阵元素已访问过 ( (3) 可合并至 (2) ) 。
- 返回 true : k = len(word) - 1 ,即字符串 word 已全部匹配。
-
递推工作:
- 标记当前矩阵元素: 将 board[i][j] 修改为 空字符 ‘’ ,代表此元素已访问过,防止之后搜索时重复访问。
- 搜索下一单元格: 朝当前元素的 上、下、左、右 四个方向开启下层递归,使用 或 连接 (代表只需找到一条可行路径就直接返回,不再做后续 DFS ),并记录结果至 res 。
- 还原当前矩阵元素: 将 board[i][j] 元素还原至初始值,即 word[k] 。
-
返回值: 返回布尔量 res ,代表是否搜索到目标字符串。
代码示例
class Solution {public boolean exist(char[][] board, String word) {char[] words = word.toCharArray();for(int i = 0; i < board.length; i++) {for(int j = 0; j < board[0].length; j++) {if (dfs(board, words, i, j, 0)) return true;}}return false;}boolean dfs(char[][] board, char[] word, int i, int j, int k) {if (i >= board.length || i < 0 || j >= board[0].length || j < 0 || board[i][j] != word[k]) return false;if (k == word.length - 1) return true;board[i][j] = '\0';boolean res = dfs(board, word, i + 1, j, k + 1) || dfs(board, word, i - 1, j, k + 1) || dfs(board, word, i, j + 1, k + 1) || dfs(board, word, i , j - 1, k + 1);board[i][j] = word[k];return res;}
}
3.2 分割回文串
给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是 回文串 。返回 s 所有可能的分割方案。
示例 1:
输入:s = “aab”
输出:[[“a”,“a”,“b”],[“aa”,“b”]]
示例 2:
输入:s = “a”
输出:[[“a”]]
解法
由于需要求出字符串 sss 的所有分割方案,因此我们考虑使用搜索 + 回溯的方法枚举所有可能的分割方法并进行判断。
假设我们当前搜索到字符串的第 iii 个字符,且 s[0…i−1] 位置的所有字符已经被分割成若干个回文串,并且分割结果被放入了答案数组 ans 中,那么我们就需要枚举下一个回文串的右边界 jjj,使得 s[i…j] 是一个回文串。
因此,我们可以从 iii 开始,从小到大依次枚举 jjj。对于当前枚举的 jjj 值,我们使用双指针的方法判断 s[i…j] 是否为回文串:如果 s[i…j]是回文串,那么就将其加入答案数组 ans 中,并以 j+1j+1j+1 作为新的 i 进行下一层搜索,并在未来的回溯时将 s[i…j] 从 ans 中移除。
如果我们已经搜索完了字符串的最后一个字符,那么就找到了一种满足要求的分割方法。
代码示例
class Solution {boolean[][] f;List<List<String>> ret = new ArrayList<List<String>>();List<String> ans = new ArrayList<String>();int n;public List<List<String>> partition(String s) {n = s.length();f = new boolean[n][n];for (int i = 0; i < n; ++i) {Arrays.fill(f[i], true);}for (int i = n - 1; i >= 0; --i) {for (int j = i + 1; j < n; ++j) {f[i][j] = (s.charAt(i) == s.charAt(j)) && f[i + 1][j - 1];}}dfs(s, 0);return ret;}public void dfs(String s, int i) {if (i == n) {ret.add(new ArrayList<String>(ans));return;}for (int j = i; j < n; ++j) {if (f[i][j]) {ans.add(s.substring(i, j + 1));dfs(s, j + 1);ans.remove(ans.size() - 1);}}}
}
3.3 全排列
给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
示例 1:
输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
示例 2:
输入:nums = [0,1]
输出:[[0,1],[1,0]]
示例 3:
输入:nums = [1]
输出:[[1]]
代码示例
class Solution {public List<List<Integer>> permute(int[] nums) {List<List<Integer>> result = new ArrayList<>();List<Integer> input = new ArrayList<>();for (int i: nums) input.add(i);List<Integer> tmp = new ArrayList<>();dfs(input, result, tmp, nums.length);return result;}public void dfs(List<Integer> input, List<List<Integer>> result, List<Integer> temp, int k){if(temp.size()==k) {result.add(temp);return;}for(int i=0;i<input.size();i++){List<Integer> copyInput=new ArrayList<>(input);List<Integer> copyTemp=new ArrayList<>(temp);copyTemp.add(input.get(i));copyInput.remove(i);dfs(copyInput,result,copyTemp,k);}}
}
3.4 子集
给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
示例 1:
输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
示例 2:
输入:nums = [0]
输出:[[],[0]]
解法思路
整体上跟跳跃游戏一致,只是稍作变通
代码示例
class Solution {public List<List<Integer>> subsets(int[] nums) {List<List<Integer>> res = new ArrayList<>();dfs(0, nums, res,new ArrayList<Integer>());return res;}public void dfs(int i, int[] nums, List<List<Integer>> res, ArrayList<Integer> tmp) {res.add(new ArrayList<>(tmp));for(int j = i; j < nums.length;j++) {tmp.add(nums[j]);dfs(j + 1, nums, res, tmp);tmp.remove(tmp.size() -1);}}
}
3.5 电话号码的字母组合
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
示例 1:
输入:digits = “23”
输出:[“ad”,“ae”,“af”,“bd”,“be”,“bf”,“cd”,“ce”,“cf”]
示例 2:
输入:digits = “”
输出:[]
示例 3:
输入:digits = “2”
输出:[“a”,“b”,“c”]
代码示例
class Solution {public List<String> letterCombinations(String digits) {// 定义相关数字和字母的mapping关系Map<String, String[]> map = new HashMap<String, String[]>(){{put("2", new String[]{"a","b","c"});put("3", new String[]{"d","e","f"});put("4", new String[]{"g","h","i"});put("5", new String[]{"j","k","l"});put("6", new String[]{"m","n","o"});put("7", new String[]{"p","q","r","s"});put("8", new String[]{"t","u","v"});put("9", new String[]{"w","x","y","z"});}};// 逃出条件1:什么都没有输入,返回空if(digits.length() == 0) {return new ArrayList<>();}// 逃出条件2: 只输入1个数字,返回对应的mapping字母列表if(digits.length() == 1) {List<String> arrayList = new ArrayList<>(map.get(digits).length);Collections.addAll(arrayList, map.get(digits));return arrayList;}String[] strList = digits.split("");// 构造递归条件,将完整的输入拆分成2部分String[] first = map.get(strList[0]);String rest = "";for(int i = 1;i < strList.length;i++) {rest += strList[i];}List<String> result = new ArrayList<>();// 分别进行拼凑,进行递归所有的数字for(int i=0;i<first.length;i++) {List<String> tmp = letterCombinations(rest);for(int j = 0;j<tmp.size();j++) {result.add(first[i] + tmp.get(j));}}return result;}
}
3.6 组合总和
给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。
candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。
对于给定的输入,保证和为 target 的不同组合数少于 150 个。
示例 1:
输入:candidates = [2,3,6,7], target = 7
输出:[[2,2,3],[7]]
解释:
2 和 3 可以形成一组候选,2 + 2 + 3 = 7 。注意 2 可以使用多次。
7 也是一个候选, 7 = 7 。
仅有这两种组合。
示例 2:
输入: candidates = [2,3,5], target = 8
输出: [[2,2,2,2],[2,3,3],[3,5]]
示例 3:
输入: candidates = [2], target = 1
输出: []
代码示例
class Solution {public List<List<Integer>> combinationSum(int[] candidates, int target) {List<List<Integer>> res = new ArrayList<>();Arrays.sort(candidates);dfs(candidates, target, res, 0, new ArrayList<Integer>());return res;}public void dfs(int[] candidates, int target, List<List<Integer>> res, int i ,List<Integer> tmp) {if(target < 0) return;if(target == 0) {res.add(new ArrayList<>(tmp));return;} for(int start = i; start < candidates.length;start++) {tmp.add(candidates[start]);dfs(candidates, target - candidates[start], res,start, tmp);tmp.remove(tmp.size() - 1);}}
}
3.7 括号生成
数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。
示例 1:
输入:n = 3
输出:[“((()))”,“(()())”,“(())()”,“()(())”,“()()()”]
示例 2:
输入:n = 1
输出:[“()”]
代码示例
class Solution(object):def generateParenthesis(self, n):""":type n: int:rtype: List[str]"""""":type n: int:rtype: List[str]"""global resultresult = []bracket_str = ""self.find_bracket(n, n, bracket_str)print(result)return resultdef find_bracket(self, left, right, bracket_str):""""""global resultif left == 0 and right == 0:result.append(bracket_str)if left > right:returnif left > 0:self.find_bracket(left - 1, right, bracket_str + '(')if right > 0:self.find_bracket(left, right - 1, bracket_str + ')')
4. 参考文档
暂无,相关面试题请参考leetcode以及相关说明