十三、回溯法
1、知识讲解
DFS 和回溯算法区别
DFS 是一个劲的往某一个方向搜索,而回溯算法建立在 DFS 基础之上的,但不同的是在搜索过程中,达到结束条件后,恢复状态,回溯上一层,再次搜索。因此回溯算法与 DFS 的区别就是有无状态重置
何时使用回溯算法
当问题需要 "回头",以此来查找出所有的解的时候,使用回溯算法。即满足结束条件或者发现不是正确路径的时候(走不通),要撤销选择,回退到上一个状态,继续尝试,直到找出所有解为止
怎么样写回溯算法(从上而下,※代表难点,根据题目而变化)
①画出递归树,找到状态变量(回溯函数的参数),这一步非常重要※
②根据题意,确立结束条件
③找准选择列表(与函数参数相关),与第一步紧密关联※
④判断是否需要剪枝
⑤作出选择,递归调用,进入下一层
⑥撤销选择
回溯问题的类型
类型 | |
子集、组合 | |
全排列 | |
搜索 |
注意:子集、组合与排列是不同性质的概念。子集、组合是无关顺序的,而排列是和元素顺序有关的,如 [1,2] 和 [2,1] 是同一个组合(子集),但 [1,2] 和 [2,1] 是两种不一样的排列!!!!因此被分为两类问题
也可以用BFS的思路
2、题型
1.组合类问题,共有n个元素
模板(DFS)
创建结果数组+中间变量数组
构造helper实现递归(if + else)
- 终止条件+添加中间变量数组到结果数组中(重新复制一个)
- 递归部分:
- 不取: helper中的索引下移一位
- 取+放回: 当前i加到中间变量数组中
子集问题
长度无限制,可以为空集
终止条件:index到尾 迭代继续:index 未到尾
剑指 Offer II 079. 所有子集 数组中的元素 互不相同
class Solution {
public List<List<Integer>> subsets(int[] nums) {
//创建结果数组
List<List<Integer>> res = new LinkedList<>();
LinkedList<Integer> subset = new LinkedList<>();
helper(nums, 0, res, subset);
return res;
}
private void helper(int[] nums, int i, List<List<Integer>> res, LinkedList<Integer> subset) {
if (i == nums.length) {
res.add(new LinkedList<>(subset));
} else if (i < nums.length) {
//不放
helper(nums, i + 1, res, subset);
//放
subset.add(nums[i]);
helper(nums, i + 1, res, subset);
subset.removeLast();//放后取出来
}
}
}
90. 子集 II 可能包含重复元素
法一:先排序,在递归中去重(剪枝)
法二:模仿子集I,后去重
public List<List<Integer>> subsetsWithDup(int[] nums) {
List<List<Integer>> res = new LinkedList<>();
LinkedList<Integer> temp = new LinkedList<>();
Arrays.sort(nums);
dfs(res, temp, 0, nums, false);
return res;
}
private void dfs(List<List<Integer>> res, LinkedList<Integer> temp, int i, int[] nums, boolean choosePre) {
if (i == nums.length) {
res.add(new LinkedList<>(temp));
} else {
dfs(res, temp, i + 1, nums, false);
// 剪枝
if (i != 0 && nums[i] == nums[i-1] && !choosePre) {
return;
}
temp.add(nums[i]);
dfs(res, temp, i + 1, nums, true);
temp.removeLast();
}
}
public List<String> letterCasePermutation(String s) {
List<String> res = new LinkedList<>();
char[] chars = s.toCharArray();
dfs(s, 0, res, chars);
return res;
}
private void dfs(String s, int i, List<String> res, char[] chars) {
if (i == s.length()) {
res.add(new String(chars));
} else {
char ch = chars[i];
if (Character.isLetter(ch)) {
chars[i] = Character.toUpperCase(ch);
dfs(s, i + 1, res, chars);
chars[i] = Character.toLowerCase(ch);
dfs(s, i + 1, res, chars);
} else {
dfs(s, i + 1, res, chars);
}
}
}
组合问题
长度有限制,必须==k
终止条件:长度==k 迭代继续:index 未到尾
注意及时剪枝
public List<List<Integer>> combine(int n, int k) {
List<List<Integer>> res = new LinkedList<>();
LinkedList<Integer> com = new LinkedList<>();
dfs(1, n, k, com, res);
return res;
}
void dfs(int i, int n, int k, LinkedList<Integer> com, List<List<Integer>> res) {
if (com.size() == k) {
res.add(new LinkedList<>(com));
} else if (i <= n && n - i + 1 >= k - com.size()) {
dfs(i + 1, n, k, com, res);
com.add(i);
dfs(i + 1, n, k, com, res);
com.removeLast();
}
}
拓展:求和值有限制
选取元素可重复,i可不变
class Solution {
public List<List<Integer>> combinationSum(int[] candidates, int target) {
List<List<Integer>> res = new LinkedList<>();
LinkedList<Integer> comb = new LinkedList<>();
helper(0, target, candidates, res, comb);
return res;
}
private void helper(int i, int target, int[] candidates, List<List<Integer>> res, LinkedList<Integer> comb) {
if (target == 0) {
res.add(new LinkedList<>(comb));
} else if (i < candidates.length && target > 0) {
helper(i + 1, target, candidates, res, comb);
if (target - candidates[i] >= 0) {
comb.add(candidates[i]);
helper(i, target - candidates[i], candidates, res, comb);
comb.removeLast();
}
}
}
}
选取元素不可重复,i= i+1
包含重复元素,要求选取不重复,组合不重复:增加一个找下一个不重复元素的函数(或者用dp存储)
剑指 Offer II 082. 含有重复元素集合的组合 = 40. 组合总和 II
class Solution {
private int[] dp;
private int[] counts;
//计数排序
public int[] countSort(int[] arr) {
//遍历arr
counts = new int[51];
for (int num : arr) {
// if (num < 31) counts[num]++;
counts[num]++;
}
//遍历counts
int i = 0;
int temp = 0;
for (int num = 1; num < counts.length; num++) {
temp += counts[num];
while (counts[num] > 0) {
arr[i] = num;
dp[i] = temp;
counts[num]--;
i++;
}
}
return arr;
}
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
List<List<Integer>> res = new LinkedList<>();
LinkedList<Integer> comb = new LinkedList<>();
dp = new int[candidates.length];
countSort(candidates);
helper(0, target, candidates, res, comb);
return res;
}
private void helper(int i, int target, int[] candidates, List<List<Integer>> res, LinkedList<Integer> comb) {
if (target == 0) {
res.add(new LinkedList<>(comb));
} else if (i < candidates.length && target > 0) {
helper(dp[i], target, candidates, res, comb);
if (target - candidates[i] >= 0) {
comb.add(candidates[i]);
helper(i+1, target - candidates[i], candidates, res, comb);
comb.removeLast();
}
}
}
}
其他:分组交叉组合
17. 电话号码的字母组合 BFS
class Solution {
public List<String> letterCombinations(String digits) {
List<String> res = new LinkedList<>();
if (digits.length() == 0) {
return res;
}
StringBuffer temp = new StringBuffer();
Map<Character, String> phoneMap = new HashMap<Character, String>() {{
put('2', "abc");
put('3', "def");
put('4', "ghi");
put('5', "jkl");
put('6', "mno");
put('7', "pqrs");
put('8', "tuv");
put('9', "wxyz");
}};
helper(digits, 0, res, temp, phoneMap);
return res;
}
private void helper(String digits, int i, List<String> res, StringBuffer temp, Map<Character, String> phoneMap) {
if (i == digits.length()) {
res.add(temp.toString());//深拷贝还是浅拷贝
} else if (i < digits.length()) {
char num = digits.charAt(i);
for (char ch : phoneMap.get(num).toCharArray()) {
temp.append(ch);
helper(digits, i + 1, res, temp, phoneMap);
temp.deleteCharAt(temp.length() - 1);
}
}
}
}
2.排列类问题,共有n个元素
模板(BFS)
创建结果数组
构造helper实现递归(if + else)
终止条件(i==length)+构造新数组(for循环)插入到结果数组中
递归部分:
for循环(i之后的每一个)
交换 i 和 j
递归helper(i+1)
交换 i 和 j
无重复元素,全排列问题
剑指 Offer II 083. 没有重复元素集合的全排列 = 46. 全排列
class Solution {
public List<List<Integer>> permute(int[] nums) {
List<List<Integer>> pers = new LinkedList<>();
helper(0, pers, nums);
return pers;
}
private void helper(int i, List<List<Integer>> pers, int[] nums) {
if (i == nums.length) {
List<Integer> per = new LinkedList<>();
for (int num : nums) {
per.add(num);
}
pers.add(per);
} else {
for (int j = i; j < nums.length; j++) {
swap(nums, i, j);
helper(i + 1, pers, nums);
swap(nums, i, j);
}
}
}
private void swap(int[] nums, int i1, int i2) {
if (i1 != i2) {
int temp = nums[i1];
nums[i1] = nums[i2];
nums[i2] = temp;
}
}
}
有重复元素,全排列问题
剑指 Offer II 084. 含有重复元素集合的全排列 = 47. 全排列 II
用一个哈希表存储是否交换过
class Solution {
public List<List<Integer>> permuteUnique(int[] nums) {
//结果
List<List<Integer>> res = new LinkedList<>();
helper(nums, 0, res);
return res;
}
private void helper(int[] nums, int i, List<List<Integer>> res) {
if (i == nums.length) {
List<Integer> temp = new LinkedList<>();
for (int num : nums) {
temp.add(num);
}
res.add(temp);
} else {
Set<Integer> set = new HashSet<>();
for (int j = i; j < nums.length; j++) {
if(!set.contains(nums[j])){
set.add(nums[j]);
swap(nums, i, j);
helper(nums, i + 1, res);
swap(nums, i, j);
}
}
}
}
private void swap(int[] nums, int i1, int i2) {
if (i1 != i2) {
int temp = nums[i1];
nums[i1] = nums[i2];
nums[i2] = temp;
}
}
}
3.多步骤+多选项
回溯+剪枝
终止条件(l=r=0)
递归分支
- l > 0
- r: l < r
class Solution {
public List<String> generateParenthesis(int n) {
List<String> res = new LinkedList<>();
helper(res, n, n, "");
return res;
}
private void helper(List<String> res, int l, int r, String s) {
if (l == 0 && r == 0) {
res.add(s);
}
//“(”只需要有就行
if (l > 0) {
helper(res, l - 1, r, s + "(");
}
if (l < r) {
helper(res, l, r - 1, s + ")");
}
}
}
法一:回溯+判断回文函数
class Solution {
public List<List<String>> partition(String s) {
//1.根据返回值类型创建结果变量
List<List<String>> res = new LinkedList<>();
//2.创建临时变量(可不创)
//3.调用递归函数,写出递归方程
helper(s, 0, res, new LinkedList<String>());
return res;
}
private void helper(String s, int i, List<List<String>> res, LinkedList<String> temp) {
//递归终止条件
if (i == s.length()) {
res.add(new LinkedList<>(temp));
}
//递归继续
for (int j = i; j < s.length(); j++) {
if (isPalindrome(s, i, j)) {
temp.add(s.substring(i, j + 1));//左闭右开
helper(s, j + 1, res, temp);
temp.removeLast();
}
}
}
//判断是否是回文串,左闭右闭
private boolean isPalindrome(String s, int start, int end) {
while (start < end) {
if (s.charAt(start++) != s.charAt(end--)) {
return false;
}
}
return true;
}
}
法二:回溯+预处理(中心扩展法)
class Solution {
public List<List<String>> partition(String s) {
List<List<String>> res = new ArrayList<>();
int len = s.length();
boolean[][] dp = new boolean[len][len];
for(int i = 0; i < len; i++){
prePro(s, i, i, dp);
prePro(s, i, i + 1, dp);
}
helper(res, new ArrayList<>(), s, 0, dp);
return res;
}
//进行预处理,利用中心扩展 将所有回文子串的位置存储到 dp 中
private void prePro(String s, int left , int right, boolean[][] dp){
while(left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)){
dp[left][right] = true;
left--;
right++;
}
}
private void helper(List<List<String>> res, List<String> list, String s, int index, boolean[][] dp){
if(index == s.length()){
res.add(new ArrayList<>(list));
return;
}
for(int i = index; i < s.length(); i++){
//利用预处理结果就不用再去判断该字符串是否是回文串
if(!dp[index][i]){
continue;
}
list.add(s.substring(index, i + 1));
helper(res, list, s, i + 1, dp);
list.remove(list.size() - 1);
}
}
}
剑指 Offer II 087. 复原 IP
class Solution {
public List<String> restoreIpAddresses(String s) {
List<String> res = new LinkedList<>();
helper(s, res, 0, 0, "", "");
return res;
}
private void helper(String s, List<String> res, int i, int segI, String seg, String ip) {
if (i == s.length() && segI == 3 && isValidSeg(seg)) {
res.add(ip + seg);
} else if (i < s.length() && segI <= 3) {
char ch = s.charAt(i);
if (isValidSeg(seg + ch)) {
helper(s, res, i + 1, segI, seg + ch, ip);
}
if (seg.length() > 0 && segI < 3) {
helper(s, res, i + 1, segI + 1, "" + ch, ip + seg + ".");
}
}
}
private boolean isValidSeg(String seg) {
return Integer.valueOf(seg) <= 255
&& (seg.equals("0") || seg.charAt(0) != '0');
}
}