搞了一天的回溯算法,现在总结一下,基本的解题思路还是有一定套路的。
回溯算法
采用试错的思想,它尝试分步的去解决一个问题。在分步解决问题的过程中,当它通过尝试发现现有的分步答案不能得到有效的正确的解答的时候,它将取消上一步甚至是上几步的计算,再通过其它的可能的分步解答再次尝试寻找问题的答案。回溯法通常用最简单的递归方法来实现,在反复重复上述的步骤后可能出现两种情况:
找到一个可能存在的正确的答案;
在尝试了所有可能的分步方法后宣告该问题没有答案。
回溯算法本质上是一种递归算法,基本上是用于解决对于某种集合的遍历枚举搜索,可以使用预先排序的方式按照某种规则来进行剪枝,以减少搜索路径。一般情况下是对于一颗树的搜索,从根节点出发,向符合条件的第一个子节点出发搜索,然后以当前子节点作为根节点 继续搜索,如果找到符合题意的解则记录下来,如果找不到 则从当前子节点返回上一级父节点,从父节点的第二个可行子节点进行搜索。因此解决回溯算法,最重要的几个关键点:
1.画出整个搜索树,根节点是什么,每个节点的可选子节点是什么。
2.从根节点开始向下搜索,如果符合条件 则记录结果集,如果不符合 则继续搜索,如果都不符合,则从该节点回退到上一个节点,进行该过程。因此应该明确,搜索过程遇到什么条件可以停止,一种是找到符合题意的解,一种是遍历到临界条件。
3.是否需要提前处理待搜索集合,例如排序等。
4.是否需要记录待搜索集合的每个元素的状态,避免重复搜索等。
1.子集
给你一个整数数组 nums
,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
以[1,2,3]为例,其结果为[] [1] [1,2] [1,2,3] [1, 3] [2] [2,3] [3]
整颗搜索树从根节点(空)出发,考虑根节点的可选子节点是1,2,3 然后首先考虑子节点1的可选子节点是2,3,然后考虑2的可选子节点是3,3的可选子节点为空 则发现子集为 空 1 1,2 1,2,3 ,此时一直回溯到根节点空处,从第二个子节点2开始搜索,。。。
/**
* 给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
* 解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
* */
public List<List<Integer>> subsets(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
if (nums.length == 0) {
return result;
}
// 首先加入空集
result.add(new ArrayList<>());
backTrace(result, new ArrayList<>(), nums, 0);
return result;
}
private void backTrace(List<List<Integer>> result, List<Integer> path, int[] nums, int index) {
// 如果寻找位置=数组长度 则直接返回
if (index == nums.length) return;
// 从index位置开始加入结果集
for (int i=index; i<nums.length; i++) {
path.add(nums[i]);
result.add(new ArrayList<>(path));
// 当前位置加入后 尝试加入下一个位置数字
backTrace(result, path, nums, i+1);
// 回溯 当前位置取消 尝试index+1位置
path.remove(path.size()-1);
}
}
利用index来标记当前搜索到数组的位置,搜索停止的条件是 index==nums.length
从节点index开始 其可选的子节点的范围是 [index+1,nums.length] 因为是子集 当前元素已经处理时后续不能再继续处理。
2.子集II
给你一个整数数组 nums ,其中可能包含重复元素,请你返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。
该题目中由于元素是重复的,因此要考虑子集的重复。
解决重复问题的思路 一般首先将集合排序 并且建立状态变量来记录每个元素是否已经被搜索过,在搜索过程中要考虑当前节点和上一个节点直接是否相等 是否会产生重复解。
以子集[1,2,2]为例 根节点出发 可选子节点有1 2 2 从1 出发 可选的子节点有 2 2 由于两个子节点重复,因此处理完第一个子节点2 时,后一个不需要处理,其条件是第一个子节点处理完之后 下一个子节点和第一个子节点相同 则下一个子节点可以直接跳过。
/**
* 给你一个整数数组 nums ,其中可能包含重复元素,请你返回该数组所有可能的子集(幂集)。
* 解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。
* */
public List<List<Integer>> subsetsWithDup(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
if (nums.length == 0) {
return result;
}
// 首先加入空集
result.add(new ArrayList<>());
Arrays.sort(nums);
boolean[] used = new boolean[nums.length];
backTrace2(result, new ArrayList<>(), nums, 0, used);
return result;
}
private void backTrace2(List<List<Integer>> result, List<Integer> path, int[] nums, int index, boolean[] used) {
if (index == nums.length) return;
for (int i=index; i<nums.length; i++) {
// 如果当前元素等于上一个元素 并且上一个元素没有被使用过(实际上已经使用过被回溯复原的) 该该元素直接跳过即可
if (i>0 && nums[i]==nums[i-1] && !used[i-1]) continue;
// 如果当前元素没有使用过
if (!used[i]) {
path.add(nums[i]);
used[i] = true;
result.add(new ArrayList<>(path));
backTrace2(result, path, nums, i+1, used);
path.remove(path.size()-1);
used[i] = false;
}
}
}
利用index来表示搜索数组的位置 利用used数组来表示该元素是否被搜索过。上述重复跳过的条件是当前子节点处理完之后 下一个子节点和当前子节点相同 则下一个子节点可以直接跳过
if (i>0 && nums[i]==nums[i-1] && !used[i-1]) continue;
第一个子节点处理完之后 由于回溯 其used=false; 如果上一个元素used=true 则当前节点不可跳过 其情况为 1,2,2这种情况,即上一个结果正在被使用中,当前节点也可以加入到结果集中。
3.全排列
给定一个不含重复数字的数组 nums
,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案
给定1,2,3
从根节点出发 其可选的子节点为1,2,3
考虑节点1 其可选的子节点为2,3 考虑节点2 其可选子节点为3 考虑3无可选子节点 则找到一条可行解。
考虑节点2 其可选的子节点为1,3 。。。
考虑节点3 其可选的子节点为1,2.。。。
搜索停止的条件是 index==nums.length
对于index位置来说,其可选的子节点为[0, nums.length]并且之前搜索路径上没有节点 利用used数组来记录搜索使用过的节点。
/**
* 给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
* */
public List<List<Integer>> permute(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
if (nums.length == 0) {
return result;
}
boolean[] used = new boolean[nums.length];
backTrace(result, new ArrayList<>(), 0, nums, used);
return result;
}
private void backTrace(List<List<Integer>> result, List<Integer> path, int index, int[] nums, boolean[] used) {
if (index == nums.length) {
result.add(new ArrayList<>(path));
return;
}
// 对于位置index数字从0开始尝试探索
for (int i=0; i<nums.length; i++) {
// 如果该数字没有被使用过 如果被使用过则跳过
if (!used[i]) {
path.add(nums[i]);
used[i] = true;
// 继续寻找index+1位置的数字
backTrace(result, path, index+1, nums, used);
path.remove(path.size() - 1);
used[i] = false;
}
}
}
对于位置index来说都从0开始尝试搜索所欲可能 但要排除当前已经使用过的元素
4.全排列II
给定一个可包含重复数字的序列 nums
,按任意顺序 返回所有不重复的全排列。
去除重复 将数组排序 如果当前元素=上一个元素 并且上一个元素已经在当前搜索路径下 则当前元素可跳过
/**
* 给定一个可包含重复数字的序列 nums ,按任意顺序返回所有不重复的全排列。
* */
public List<List<Integer>> permuteUnique(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
if (nums.length == 0) {
return result;
}
Arrays.sort(nums);
boolean[] used = new boolean[nums.length];
backTrace2(result, new ArrayList<>(), 0, nums, used);
return result;
}
private void backTrace2(List<List<Integer>> result, List<Integer> path, int index, int[] nums, boolean[] used) {
if (index == nums.length) {
result.add(new ArrayList<>(path));
return;
}
for (int i=0; i<nums.length; i++) {
// 由于数组是有序的 如果当前元素和上一个是相同的 并且上一个元素没有被使用过(实际上已经被使用过 被回溯复原的) 则跳过
// 假定重复数组元素为1(A),1(B),1(C) 执行下面判断条件后保证了排列顺序为ABC 如果首先取值为B 则因为A并没有在B之前取到,因此直接跳过 保证了ABC的访问顺序唯一 ABC 不会出现BAC等情况。
if (i > 0 && nums[i] == nums[i-1] && !used[i-1]) continue;
if (!used[i]) {
path.add(nums[i]);
used[i] = true;
backTrace2(result, path, index+1, nums, used);
path.remove(path.size()-1);
used[i] = false;
}
}
}
5.组合求和
给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。candidates 中的数字可以无限制重复被选取。
从根节点出发 其可选的子节点为数组全集,依次遍历每一个位置index 记录搜索路径的和。
对于当前位置index 其可选的子节点为[index, nums.length]因为数字可以重复使用。
搜索停止的条件是index==nums.length 或者sum(path)=target
如果将数组提前排序 则当前元素如果大于target 则可以提前停止搜索,因为所有输入都为正数,后续sum(path)必将会大于target,只能回溯尝试其他解。
/**
* 给定一个无重复元素的数组 candidates 和一个目标数 target ,
* 找出 candidates 中所有可以使数字和为 target 的组合。
*
* candidates 中的数字可以无限制重复被选取。
* */
public List<List<Integer>> combinationSum(int[] candidates, int target) {
List<List<Integer>> result = new ArrayList<>();
Arrays.sort(candidates);
backTrack(result, new ArrayList<>(), candidates, target, 0);
return result;
}
private void backTrack(List<List<Integer>> result, List<Integer> path, int[] candidates,
int target, int index) {
// 如果当前path之前=目标值 将path放入结果集中
if (target == 0) {
// 这里要赋值一份
result.add(new ArrayList<>(path));
return;
}
// 从index位置开始尝试 如果
for (int i=index; i<candidates.length; i++) {
// 由于数组是有序的 如果当前数字大于target 则停止搜索
if (candidates[i] > target) break;
// 将当前数字加入path
path.add(candidates[i]);
// 由于数组可以重复使用 下一个数字还是从i开始尝试
backTrack(result, path, candidates, target-candidates[i], i);
// 如果当前数字不满足 则回退到不加入该数字状态 继续尝试下一个数字
path.remove(path.size()-1);
}
}
6.组合求和II
给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的每个数字在每个组合中只能使用一次。
/**
* 给定一个数组 candidates 和一个目标数 target ,
* 找出 candidates 中所有可以使数字和为 target 的组合。
* candidates 中的每个数字在每个组合中只能使用一次。
*
* */
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
List<List<Integer>> result = new ArrayList<>();
if (candidates.length == 0) {
return result;
}
Arrays.sort(candidates);
backTrace2(result, new ArrayList<>(), target, 0, candidates);
return result;
}
private void backTrace2(List<List<Integer>> result, List<Integer> path, int target, int index, int[] candidates) {
if (target == 0) {
result.add(new ArrayList<>(path));
return;
}
for (int i=index; i<candidates.length; i++) {
if (candidates[i] > target) break;
// 避免重复 排序之后 如果当前元素和上一个一样 则跳过
if(i > index && candidates[i] == candidates[i-1]) continue;
path.add(candidates[i]);
backTrace2(result, path, target-candidates[i], i+1, candidates);
path.remove(path.size() - 1);
}
}
由于数组中数字可能会重复 ,将数组排序 搜索过程中如果当前元素等于上一个元素 则可以直接跳过;
由于每个数组中数字只能用一次,因此位置index可选的子节点集合为[index+1, nums.length]
7.有效的括号
数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。
示例 1:
输入:n = 3
输出:["((()))","(()())","(())()","()(())","()()()"]
示例 2:
输入:n = 1
输出:["()"]
有效解的条件之一是括号字符串的长度=n*2
最终有效的字符串必定有 左括号数量=右括号数量=n
从某个节点出发,如果左括号数量<n 则优先放入左括号 如果左括号数量大于右括号 则放入右括号
/**
* 数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合
*
* */
public List<String> generateParenthesis(int n) {
List<String> result = new ArrayList<>();
backTrack(result, new StringBuilder(), 0, 0, n);
return result;
}
private void backTrack(List<String> result, StringBuilder sb, int open, int close, int max) {
// 如果匹配成功 则结果长度必定为max*2
if (sb.length() == max * 2) {
result.add(sb.toString());
}
// 如果左括号个数小于max 则优先放置左括号
if (open < max) {
sb.append("(");
backTrack(result, sb, open+1, close, max);
sb.deleteCharAt(sb.length()-1);
}
// 这里必定是左括号个数>=max 此时应该放置右括号 保证字符串平衡
if (open > close) {
sb.append(")");
backTrack(result, sb, open, close+1, max);
sb.deleteCharAt(sb.length()-1);
}
}
8.可能的电话号码
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
给定字符串234 利用index记录当前位置
搜索停止条件是 index=nums.length
从跟节点出发 可选的子节点为 2 考虑节点2 其可选值为abc 考虑2的可选子节点为 3 4 考虑3可选子节点为4
/**
* 给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
*
* 给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
* */
public List<String> letterCombinations(String digits) {
List<String> result = new ArrayList<>();
if (digits.length() == 0) {
return result;
}
Map<Character, List<Character>> map = new HashMap<>();
map.put('2', Arrays.asList('a','b','c'));
map.put('3', Arrays.asList('d','e','f'));
map.put('4', Arrays.asList('g','h','i'));
map.put('5', Arrays.asList('j','k','l'));
map.put('6', Arrays.asList('m','n','o'));
map.put('7', Arrays.asList('p','q','r','s'));
map.put('8', Arrays.asList('t','u','v'));
map.put('9', Arrays.asList('w','x','y','z'));
backTrack(digits, 0, map, result, new StringBuilder());
return result;
}
private void backTrack(String str, int index, Map<Character, List<Character>> map, List<String> result, StringBuilder sb) {
// 如果后面只有没有字符 则直接将结果加入到结果集中
if (index == str.length()) {
result.add(sb.toString());
return;
}
// 取出当前字符代表的字符数组
char ch = str.charAt(index);
List<Character> list = map.get(ch);
for (char c: list) {
// 将该字符加入
sb.append(c);
backTrack(str, index+1, map, result, sb);
// 回退该字符
sb.deleteCharAt(sb.length()-1);
}
}
思路总结
给定待搜索的集合 一般为一位数组 或者二维数组 如果是一维数组 在一般用index变量来标记当前搜索位置,其下一个可选的位置一般为index或者index+1;如果是二维数组,一般使用(x,y)来标记当前搜索位置,其下一个可选位置一般为上下左右四个方向探索;
利用path来记录搜索路径 如果path是个可行解 则加入到result中;
输入待搜索集合nums 以及搜索位置index,分析搜索停止的条件 一般为index==nums.length或者根据题目分析停止条件;
从某一节点出发 考虑可行的子节点列表 如果需要去重 可以考虑集合nums首先排序,然后利用used数组来记录在搜索path中每个元素是否被使用过 剪枝操作;
对于当前位置index 可选位置有n个 从子节点0出发 将子节点0加入到路径中 继续递归当前节点0的可选子节点,然后将当前子节点0从路径中去除;从子节点1出发。。。。直到子节点n;
基本解题套路:
定义结果集result 定义搜索路径path 给定待搜索集合nums 记录当前搜索位置index
如果满足停止条件
将path加入到结果集result中 返回;
遍历当前位置可选子节点列表:
将子节点加入path 改变相应变量;
递归子节点;
回溯 将子节点从path中移除 回溯相应变量;
其他经典题目:
不同之处 对于某节点 其可选子节点的范围 上面几道题都是数组一维的顺序搜索,这几道为二维数组的上下左右四个方向可选探索,其实质一直。
参考地址: