基础知识
1. 回溯和递归是相辅相成,递归函数的下面就是回溯的逻辑,也就是回溯=递归+for循环
2. 回溯搜索算法:暴力搜索
- 组合问题(没有顺序):一般递归的时候会设置startindex记录遍历到哪个数字,递归的时候用i,收获的结果是叶子节点
- 切割问题:收获的结果是叶子节点,有判断是否满足切割的条件,需要startindex
- 子集问题:收获的结果是每个点,需要startindex
- 排列问题(有顺序):不同的元素顺序,是不同的组合,不需要startindex,每次从头搜索;可以用path.contains代替used数组
- 棋盘问题
3.如何理解回溯法:都可以抽象为一个n叉树形结构,宽度就是每个结点要处理的集合大小(用for循环遍历),深度就是递归的深度
4. 回溯法backtracking的模板:一般来说都没有返回值,参数比较多边用边添加
题目一:组合问题:
1. 题目:77. 组合 - 力扣(LeetCode) :给定两个整数 n 和 k,返回 1 ... n 中所有可能的 k 个数的组合
2. 思路:画出一个树形结构,得到的答案不能重复,设置一个startindex控制每次搜索的起始位置
- 参数是path(一维数组放中间遍历,全局变量)、res(二维数组放答案、全局变量)、n是一共有几个数,k是答案的长度、startindex搜索的起始位置;返回值是void
- 终止条件:path的大小为k if(path.size==k){result.add(path); return}
- 回溯逻辑:第一层每一个结点都是一个for循环,从startindex开始遍历剩下的元素,元素放入path,再从i+1递归,再把元素从path弹出
3. 注意的点:
- path添加ans中要先if (path.size()==k)ans.add(new ArrayList<>(path))
- 全局变量得是静态的才能被引用
- 剪枝操作:优化for循环,还需要选k-path.size()个元素,最多要从n-(k-path.size())+1开始搜索。比如n=4,k=3,当选了0个元素的时候,4-(3-0)+1=2,最多从2开始往后找比如234,从3开始只有34不符合要求。
class Solution {
List<List<Integer>> ans=new ArrayList<>();
List<Integer> path=new ArrayList<>();
public List<List<Integer>> combine(int n, int k) {
int index=1;
backtrack(n,k,index);
return ans;
}
public void backtrack(int n,int k,int index){
if (path.size()==k){
ans.add(new ArrayList<>(path));
return;
}
for(int i=index;i<=n - (k - path.size()) + 1;i++){
path.add(i);
backtrack(n,k,i+1);
path.remove(path.size()-1);
}
}
}
题目二:电话号码的字母组合
1. 题目:17. 电话号码的字母组合 - 力扣(LeetCode)
2. 数字到字符串的映射,二维数组或者map,用递归替代for循环也就是答案的个数
3. 递归返回值void,参数是字符串,index表示当前递归遍历到哪个数字(和前两题不一样)(startindex用于在一个集合标记收获哪些元素)
终止条件:if(index==digits.size())result.add(path)表明遍历到头了,再return
单层遍历逻辑:
先找到字符串中每个数字对应的字母集,遍历这个字母集,比如说输入的是2,对应的是[a,b,c]就遍历[a,b,c]
放入到中间结果集中;再递归
class Solution {
//设置全局列表存储最后的结果
List<String> list = new ArrayList<>();
public List<String> letterCombinations(String digits) {
if (digits == null || digits.length() == 0) {
return list;
}
//初始对应所有的数字,为了直接对应2-9,新增了两个无效的字符串""
String[] numString = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
//迭代处理
backTracking(digits, numString, 0);
return list;
}
//每次迭代获取一个字符串,所以会涉及大量的字符串拼接,所以这里选择更为高效的 StringBuilder
StringBuilder temp = new StringBuilder();
//比如digits如果为"23",num 为0,则str表示2对应的 abc
public void backTracking(String digits, String[] numString, int num) {
//遍历全部一次记录一次得到的字符串
if (num == digits.length()) {
list.add(temp.toString());
return;
}
//str 表示当前num对应的字符串
String str = numString[digits.charAt(num) - '0'];
for (int i = 0; i < str.length(); i++) {
temp.append(str.charAt(i));
//递归,处理下一层
backTracking(digits, numString, num + 1);
//剔除末尾的继续尝试
temp.deleteCharAt(temp.length() - 1);
}
}
}
题目三:组合总和|||(数组无重复,答案中k个数字不重复)
1. 题目:216. 组合总和 III - 力扣(LeetCode) :找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字
2. 代码:记得剪枝
class Solution {
int sum=0;
List<List<Integer>>ans=new ArrayList<>();
List<Integer> path=new ArrayList<>();
public List<List<Integer>> combinationSum3(int k, int n) {
backtrack(k,n,1);
return ans;
}
public void backtrack(int k,int n,int start){
if (sum==n && path.size()==k){
ans.add(new ArrayList<>(path));
return;}
if (sum>n || path.size()>k)return;
for (int i=start;i<=9-(k-path.size()+1);i++){
sum+=i;
path.add(i);
backtrack(k,n,i+1);
sum-=i;
path.remove(path.size()-1);
}
}
}
题目四:组合总和(数组无重复,答案中数字可重复)
1. 题目:39. 组合总和 - 力扣(LeetCode) 集合里的元素可以重复选,给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合
2. 回溯三部曲
- 返回值是void,参数:candidate、target、sum、startindex,全局变量:二维数组ans、一维数组path
- 边界条件:如果sum>target return,如果sum==target 将path插入到ans中 return
- 单层搜索逻辑:for(int i=startinex;i<candidates.length;i++)
class Solution {
int sum=0;
List<List<Integer>>ans=new ArrayList<>();
List<Integer> path=new ArrayList<>();
public List<List<Integer>> combinationSum(int[] candidates, int target) {
backtrack(candidates,target,0);
return ans;
}
public void backtrack(int[] nums,int target,int index){
if (sum==target){
ans.add(new ArrayList<>(path));
return;
}
if (sum>target)return;
for (int i=index;i<nums.length;i++){
path.add(nums[i]);
sum+=nums[i];
backtrack(nums,target,i);//不是index因为index一直是0
path.removeLast();
sum-=nums[i];
}
}
}
题目五:组合总和||(数组有重复,答案中数字不重复)
1. 题目: 40. 组合总和 II - 力扣(LeetCode) 给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合
2. 思路:先将数组进行排序,比如{1,1,2}
树层去重:第二个1包含的情况第一个1已经遍历过,所以第二个1不用遍历剪枝
树枝去重
3. 思路
- 全局变量:path一维数组、result二维数组;返回值void;参数:nums、target、sum、startindex、used数组标记哪些元素用过
- 边界条件:sum>target就return,sum==target就加入到ans中return
- 单层搜索:用for循环
- 树层去重:
- if(i>0 && nums[i]==nums[i-1] && used[i-1]==0) continue;跳出本次for循环
- used[i-1]==0就是说已经遍历完第一个1,开始遍历第二个1;如果used[i-1]==1表明再第一个1的分支里不能去重
class Solution {
int sum=0;
List<List<Integer>>ans=new ArrayList<>();
List<Integer> path=new ArrayList<>();
boolean[] used;
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
Arrays.sort(candidates);
used=new boolean[candidates.length];
Arrays.fill(used,false);
backtrack(candidates,target,0);
return ans;
}
public void backtrack(int[] candidates,int target,int start){
if (sum>target)return;
if (sum==target)ans.add(new ArrayList<>(path));
for (int i=start;i<candidates.length;i++){
if (i>0 && candidates[i]==candidates[i-1] &&used[i-1]==false)continue;
sum+=candidates[i];
used[i]=true;
path.add(candidates[i]);
backtrack(candidates,target,i+1);
used[i]=false;
sum-=candidates[i];
path.removeLast();
}
}
}
题目六:分割回文串
1.题目:131. 分割回文串 - 力扣(LeetCode) 给你一个字符串 s
,请你将 s
分割成一些子串,使每个子串都是 回文串(substring是左闭右开)
2. 思路
- 全局变量是ans、path;返回值void;参数是字符串、startindex控制切割的位置
- 终止条件:切割到最后一个字符串starxindex>=s.length 就将path插入到ans中,startxiindex就是切割的线
- 单层搜索逻辑:判断是否是回文,不是的话不会进行下一层递归
- 子串的范围是:(startindex,i]左闭右开,startindex是固定的
- 判断是否是回文的操作
class Solution {
int sum=0;
List<List<String>>ans=new ArrayList<>();
List<String> path=new ArrayList<>();
public List<List<String>> partition(String s) {
backtrack(s,0);
return ans;
}
public void backtrack(String s,int startindex){
if (startindex==s.length()){
ans.add(new ArrayList<>(path));
return;
}
for(int i=startindex;i<s.length();i++){
if(huiwen(s,startindex,i)==true) {
path.add(s.substring(startindex,i+1));//substring是左闭右开的
backtrack(s,i+1);
path.remove(path.size()-1);
}else continue;
}
}
public boolean huiwen(String s,int start,int end){
while (start<=end){
if (s.charAt(start)!=s.charAt(end))return false;
else {
start++;
end--;
}
}
return true;
}
}
题目七:复原IP地址
1. 题目:93. 复原 IP 地址 - 力扣(LeetCode)
2. 思路:因为数字过大,所以不能用Integer.parseint
- 全局变量:ans;返回值void;参数是:s、startindex(切割线来规定下一层递归从哪开始)、pointsum(加上.来分割)
- 终止条件:if(pointsum==3){}也就是树的深度是3,逗点是对前一个区间的字符串的合法性判断
- 对最后一个进行合法性判断:isvalid(s,startindex,s,size()-1)是左闭右闭区间,判断数字前面有没有0,有没有超过255,没有非法字符
- 最后将s放入结果集,再return
- 递归逻辑:for(int i=startindex;i<sisize()-1;i++)
- 对放入的字符串进行合法性判断:[startindex,i]左闭右闭
- 将子字符串的后面加上.后,pointsum++
- 下一层递归backtrack(s,i+2,pointsum)因为加了一个逗点
- 回溯:删除逗点,pointsum--
- 对放入的字符串进行合法性判断:[startindex,i]左闭右闭
class Solution {
List<String> ans=new ArrayList<>();
public List<String> restoreIpAddresses(String s) {
StringBuilder sb=new StringBuilder(s);
backtrack(0,0,sb);
return ans;
}
public void backtrack(int startindex,int sumpoint,StringBuilder sb){
if (sumpoint==3){
if (isvalid(sb,startindex,sb.length()-1)) {
ans.add(sb.toString());
}
return;//不管怎么样都返回
}
for (int i=startindex;i<sb.length();i++){
if (isvalid(sb,startindex,i)){
sb.insert(i+1,'.');
sumpoint++;
backtrack(i+2,sumpoint,sb);//回溯i+2的位置
sb.deleteCharAt(i+1);
sumpoint--;
}else continue;
}
}
public boolean isvalid(StringBuilder s,int start,int end){
if(start > end) return false;
if(s.charAt(start) == '0' && start != end) return false;
//else if (Integer.parseInt(s.substring(start,end).toString())>255)return false;
int num = 0;
for(int i = start; i <= end; i++){
int digit = s.charAt(i) - '0';
num = num * 10 + digit;
if(num > 255)
return false;
}
return true;
}
}
题目八:子集问题(每个点都要收获结果不光是叶子)
1. 题目:78. 子集 - 力扣(LeetCode) 每进入一层递归,就要把本层里的结果放入结果集
2. 思路:
- 全局变量:path、ans;返回值void;参数是数组、startindex
- 终止条件:先将path放入ans,剩余集合都是空也就是startindex>=nums.length return
- 单层搜索逻辑:for(int i=1;i<nums.length();i++)
- 路上的点都要push进path中,递归下一层,回溯
class Solution {
List<Integer> path=new ArrayList<>();
List<List<Integer>> ans=new ArrayList<>();
public List<List<Integer>> subsets(int[] nums) {
backtrack(nums,0);
return ans;
}
public void backtrack(int[] nums,int start){
ans.add(new ArrayList<>(path));//放入结果的时候要new ArrayList
if (start>=nums.length)return;
for (int i=start;i<nums.length;i++){
path.add(nums[i]);
backtrack(nums,i+1);
path.removeLast();
}
}
}
题目九:子集||
1. 题目:90. 子集 II - 力扣(LeetCode)
2. 注意:要先判断i>0再判断nums[i-1]==nums[i];used是全局变量不能再函数中重新定义
class Solution {
List<Integer> path=new ArrayList<>();
List<List<Integer>> ans=new ArrayList<>();
boolean[] used;
public List<List<Integer>> subsetsWithDup(int[] nums) {
used=new boolean[nums.length];
Arrays.fill(used,false);
Arrays.sort(nums);
backtrack(nums,0);
return ans;
}
public void backtrack(int[] nums,int start){
ans.add(new ArrayList<>(path));
if (start>=nums.length)return;
for (int i=start;i<nums.length;i++){
if (i>0&&nums[i]==nums[i-1] &&used[i-1]==false )continue;
path.add(nums[i]);
used[i]=true;
backtrack(nums,i+1);
used[i]=false;
path.removeLast();
}
}
}
题目十:递增子序列
1. 题目:代码随想录 (programmercarl.com) ,结果分布在节点上
2. 误区:不能将nums进行排序,会有其他没有的子集出现
3. 思路
- 全局变量:path、ans;返回值void;参数:nums、startindex
- 终止条件:if(path.size>1)ans.add(new ArrayList(path)) ;if(startindex==nums.length) return
- 递归逻辑: for(int i=0; i<nums.length;i++)
- 跳出条件:if(nums[i]<path.last() && path不为空 && uset中没有nums[i]) continue;
- map更新:将nums[i]放入map中;因为记录的是每一层是否取过,所以不需要回退
class Solution {
List<List<Integer>> result = new ArrayList<>();
List<Integer> path = new ArrayList<>();
public List<List<Integer>> findSubsequences(int[] nums) {
backTracking(nums, 0);
return result;
}
private void backTracking(int[] nums, int startIndex){
if(path.size() >= 2)
result.add(new ArrayList<>(path));
HashSet<Integer> hs = new HashSet<>();
for(int i = startIndex; i < nums.length; i++){
if(!path.isEmpty() && path.get(path.size() -1 ) > nums[i] || hs.contains(nums[i]))
continue;
hs.add(nums[i]);
path.add(nums[i]);
backTracking(nums, i + 1);
path.remove(path.size() - 1);
}
}
}
题目十一:全排列
1. 题目:46. 全排列 - 力扣(LeetCode)
2. 代码:
class Solution {//有used数组
List<Integer> path=new ArrayList<>();
List<List<Integer>> ans=new ArrayList<>();
boolean[] used;
public List<List<Integer>> permute(int[] nums) {
used=new boolean[nums.length];
Arrays.fill(used,false);
backtrack(nums);
return ans;
}
public void backtrack(int[] nums){
if (path.size()==nums.length)
{ans.add(new ArrayList<>(path));
return;}
for (int i=0;i<nums.length;i++){
if (used[i]==false) {
path.add(nums[i]);
used[i]=true;
backtrack(nums);
used[i]=false;
path.removeLast();
}
}
}
}
class Solution {//没有used数组
List<Integer> path=new ArrayList<>();
List<List<Integer>> ans=new ArrayList<>();
public List<List<Integer>> permute(int[] nums) {
backtrack(nums);
return ans;
}
public void backtrack(int[] nums){
if (path.size()==nums.length)
{ans.add(new ArrayList<>(path));
return;}
for (int i=0;i<nums.length;i++){
if (!path.contains(nums[i])) {
path.add(nums[i]);
backtrack(nums);
path.removeLast();
}
}
}
}
题目十二:全排列||(nums中有重复元素)
1. 题目:47. 全排列 II - 力扣(LeetCode)
2. 代码:
class Solution {
List<Integer> path=new ArrayList<>();
List<List<Integer>> ans=new ArrayList<>();
boolean[] used;
public List<List<Integer>> permuteUnique(int[] nums) {
used=new boolean[nums.length];
Arrays.fill(used,false);
Arrays.sort(nums);
backtrack(nums);
return ans;
}
public void backtrack(int[] nums){
if (path.size()==nums.length){
ans.add(new ArrayList<>(path));
return;
}
for (int i=0;i<nums.length;i++){
if (i>0 && nums[i]==nums[i-1] && used[i-1]==false )continue;
if (used[i]==false) {
path.add(nums[i]);
used[i] = true;
backtrack(nums);
path.removeLast();
used[i] = false;
}
}
}
}