回溯
回溯算法理论基础
题目分类:
回溯法也可以叫做回溯搜索法,它是一种搜索的方式。回溯是递归的副产品,只要有递归就会有回溯。
回溯法的效率
虽然回溯法很难,很不好理解,但是回溯法并不是什么高效的算法。因为回溯的本质是穷举,穷举所有可能,然后选出我们想要的答案,如果想让回溯法高效一些,可以加一些剪枝的操作,但也改不了回溯法就是穷举的本质。
组合无序,排列有序
理解回溯法
所有回溯法的问题都可以抽象为树形结构,因为回溯法解决的都是在集合中递归查找子集,集合的大小就构成了树的宽度,递归的深度就构成了树的深度。
递归要有终止条件,所以必然是一棵高度有限的树(N 叉树)。
回溯法模板
回溯三部曲:
- 返回值以及参数 函数返回值一般为 void,参数一般是先写逻辑才能确定
- 终止条件 终止条件从树中就可以看出,一般来说是搜到叶子节点
- 遍历过程 一般是在集合中递归搜索,集合的大小构成了树的宽度,递归的深度构成的树的深度
第 77 题. 组合
给定两个整数 n 和 k,返回 1 ... n 中所有可能的 k 个数的组合。
示例: 输入: n = 4, k = 2 输出: [ [2,4], [3,4], [2,3], [1,2], [1,3], [1,4], ]
解答:
class Solution {
List<List<Integer>> res = new ArrayList<>();
Deque<Integer> deque = new LinkedList<>();
public List<List<Integer>> combine(int n, int k) {
backtracking(n, k, 1);
return res;
}
private void backtracking(int n, int k, int startindex){
if(deque.size() == k){
res.add(new ArrayList<>(deque));
return;
}
for(int i = startindex; i <= n - (k - deque.size() - 1); i++){
deque.offer(i);
backtracking(n, k, i+1);
deque.pollLast();
}
}
}
216.组合总和 III
找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。
说明:
- 所有数字都是正整数。
- 解集不能包含重复的组合。
解答:
class Solution {
List<List<Integer>> res = new ArrayList<>();
Deque<Integer> deque = new LinkedList<>();
public List<List<Integer>> combinationSum3(int k, int n) {
backtracking(k, n, 1);
return res;
}
private void backtracking(int k, int n, int startindex){
if(k == 0 || n == 0){
if(k == 0 && n == 0) res.add(new ArrayList<>(deque));
return;
}
for(int i = startindex; i <= Math.min(n, 9); i++){
deque.offer(i);
backtracking(k-1, n-i, i+1);
deque.pollLast();
}
}
}
17.电话号码的字母组合
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
解答:
class Solution {
List<String> res = new ArrayList<>();
StringBuilder temp = new StringBuilder();
public List<String> letterCombinations(String digits) {
if(digits == null || digits.length() == 0) return res;
String[] s = {"","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};
backtracking(digits, 0, s);
return res;
}
private void backtracking(String digits, int num, String[] s){
if(num == digits.length()){
res.add(temp.toString());
return;
}
String str = s[digits.charAt(num) - '0'];
for(int i = 0; i < str.length(); i++){
temp.append(str.charAt(i));
backtracking(digits, num + 1, s);
temp.deleteCharAt(temp.length() - 1);
}
}
}
39. 组合总和
给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的数字可以无限制重复被选取。
说明:
- 所有数字(包括 target)都是正整数。
- 解集不能包含重复的组合。
解答:
class Solution {
List<List<Integer>> res = new ArrayList<>();
List<Integer> temp = new ArrayList<>();
public List<List<Integer>> combinationSum(int[] candidates, int target) {
if(candidates == null || candidates.length == 0) return null;
backtracking(candidates, target, 0);
return res;
}
private void backtracking(int[] candidates, int target, int startindex){
if(target < 0){
return;
} else if(target == 0){
res.add(new ArrayList<>(temp));
return;
}
for(int i = startindex; i < candidates.length; i++){
temp.add(candidates[i]);
backtracking(candidates, target-candidates[i], i);
temp.remove(temp.size() - 1);
}
}
}
40.组合总和 II
给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的每个数字在每个组合中只能使用一次。
说明: 所有数字(包括目标数)都是正整数。解集不能包含重复的组合。
解答:
// used数组
class Solution {
List<List<Integer>> res = new ArrayList<>();
LinkedList<Integer> temp = new LinkedList<>();
boolean[] used;
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
if(candidates == null || candidates.length == 0) return null;
used = new boolean[candidates.length];
// 加标志数组,用来辅助判断同层节点是否已经遍历
Arrays.fill(used, false);
Arrays.sort(candidates);
backtracking(candidates, target, 0);
return res;
}
private void backtracking(int[] candidates, int target, int startindex){
if(target < 0){
return;
} else if(target == 0){
res.add(new ArrayList<>(temp));
return;
}
for(int i = startindex; i < candidates.length; i++){
// used[i - 1] == true,说明同⼀树⽀nums[i - 1]使⽤过
// used[i - 1] == false,说明同⼀树层nums[i - 1]使⽤过
// 如果同⼀树层nums[i - 1]使⽤过则直接跳过
if(i > 0 && candidates[i] == candidates[i-1] && !used[i-1]) continue;
temp.add(candidates[i]);
used[i] = true;
// 每个节点仅能选择一次,所以从下一位开始
backtracking(candidates, target-candidates[i], i+1);
used[i] = false;
temp.removeLast();
}
}
}
// 不用used数组
class Solution {
List<List<Integer>> res = new ArrayList<>();
LinkedList<Integer> temp = new LinkedList<>();
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
if(candidates == null || candidates.length == 0) return null;
Arrays.sort(candidates);
backtracking(candidates, target, 0);
return res;
}
private void backtracking(int[] candidates, int target, int startindex){
if(target < 0){
return;
} else if(target == 0){
res.add(new ArrayList<>(temp));
return;
}
for(int i = startindex; i < candidates.length; i++){
if(i > startindex && candidates[i] == candidates[i-1]) continue;// 要对同一树层使用过的元素进行跳过
temp.add(candidates[i]);
backtracking(candidates, target-candidates[i], i+1);
temp.removeLast();
}
}
}
131.分割回文串
给定一个字符串 s,将 s 分割成一些子串,使每个子串都是回文串。
返回 s 所有可能的分割方案。
解答:
class Solution {
List<List<String>> res = new ArrayList<>();
Deque<String> deque = new LinkedList<>();
public List<List<String>> partition(String s) {
if(s == null || s.length() == 0) return null;
backtracking(s);
return res;
}
private void backtracking(String s){
if(s == null){
res.add(new ArrayList(deque));
return;
}
int len1 = s.length();
for(int i = 0; i < len1; i++){
if(ishuiwen(s.substring(0,i+1))){
deque.offer(s.substring(0,i+1));
if(i+1 < len1) backtracking(s.substring(i+1,len1));
else backtracking(null);
deque.pollLast();
}
}
}
private boolean ishuiwen(String s){
int len = s.length();
for(int i = 0; i < len/2; i++){
if(s.charAt(i) != s.charAt(len - i - 1)) return false;
}
return true;
}
}
93.复原 IP 地址
给定一个只包含数字的字符串,复原它并返回所有可能的 IP 地址格式。
有效的 IP 地址 正好由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 0),整数之间用 '.' 分隔。
解答:
class Solution {
List<String> res = new ArrayList<>();
public List<String> restoreIpAddresses(String s) {
StringBuilder sb = new StringBuilder(s);
backtracking(sb, 0, 0);
return res;
}
private void backtracking(StringBuilder s, int startindex, int pointnum){
if(pointnum == 3 && check(s, startindex, s.length())){
res.add(s.toString());
return;
}
if(pointnum > 3) return;
for(int i = startindex; i < s.length() && i < startindex + 3; i++){
if(check(s, startindex, i+1)){
s.insert(i+1,".");
backtracking(s, i+2, pointnum+1);
s.deleteCharAt(i+1);
}
}
}
private boolean check(StringBuilder s, int start, int end){
if(end-start > 3 || end > s.length() || start >= end || Integer.valueOf(s.substring(start, end)) > 255) return false;
if(s.charAt(start) == '0' && end-start > 1) return false;
return true;
}
}
78.子集
给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
说明:解集不能包含重复的子集。
解答:
class Solution {
List<List<Integer>> res = new ArrayList<>();
List<Integer> temp = new ArrayList<>();
public List<List<Integer>> subsets(int[] nums) {
if(nums == null || nums.length == 0) return null;
backtracking(nums, 0, nums.length-1);
return res;
}
private void backtracking(int[] nums, int left, int right){
if(left > right){
res.add(new ArrayList(temp));
return;
}
temp.add(nums[left]);
backtracking(nums, left+1, right);//添加nums[left]元素的集合
temp.removeLast();
backtracking(nums, left+1, right);//不添加nums[left]元素的集合
}
}
90.子集 II
给定一个可能包含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
说明:解集不能包含重复的子集。
解答:
class Solution {
List<List<Integer>> res = new ArrayList<>();
List<Integer> temp = new ArrayList<>();
public List<List<Integer>> subsetsWithDup(int[] nums) {
if(nums == null || nums.length == 0) return null;
Arrays.sort(nums);
backtracking(nums, 0);
return res;
}
private void backtracking(int[] nums, int startindex){
res.add(new ArrayList(temp));//「遍历这个树的时候,把所有节点都记录下来,就是要求的子集集合」
//停止条件就是:i < nums.length
for(int i = startindex; i < nums.length; i++){
if(i > startindex && nums[i] == nums[i-1]) continue;
temp.add(nums[i]);
backtracking(nums, i+1);
temp.removeLast();
}
}
}
491.递增子序列
给定一个整型数组, 你的任务是找到所有该数组的递增子序列,递增子序列的长度至少是 2。
解答:
// 暴力判断重复
class Solution {
List<List<Integer>> res = new ArrayList<>();
List<Integer> temp = new ArrayList<>();
public List<List<Integer>> findSubsequences(int[] nums) {
if(nums == null || nums.length == 0) return null;
backtracking(nums, 0);
return res;
}
private void backtracking(int[] nums, int startindex){
if(temp.size() > 1){
if(temp.get(temp.size()-1) < temp.get(temp.size()-2)) return;
res.add(new ArrayList(temp));
}
for(int i = startindex; i < nums.length; i++){
boolean flag = false;//判断全集是否包含相同的元素
for(int j = i-1; j >= startindex; j--){
if(nums[i] == nums[j]){
flag = true;
break;
}
}
if(flag) continue;
temp.add(nums[i]);
backtracking(nums, i+1);
temp.removeLast();
}
}
}
// Map
class Solution {
List<List<Integer>> res = new ArrayList<>();
List<Integer> temp = new ArrayList<>();
public List<List<Integer>> findSubsequences(int[] nums) {
if(nums == null || nums.length == 0) return null;
backtracking(nums, 0);
return res;
}
private void backtracking(int[] nums, int startindex){
if(temp.size() > 1){
if(temp.get(temp.size()-1) < temp.get(temp.size()-2)) return;
res.add(new ArrayList(temp));
}
Map<Integer, Integer> map = new HashMap<>();
for(int i = startindex; i < nums.length; i++){
if(map.getOrDefault(nums[i], 0) > 0) continue;
map.put(nums[i], 1);
temp.add(nums[i]);
backtracking(nums, i+1);
temp.removeLast();
}
}
}