回溯法
模板:
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
本质:
穷举出所有可能,选出想要的答案。
解决方法:
回溯法解决的问题都可以抽象为树形结构,因为回溯法解决的都是在集合中递归查找子集,集合的大小就构成了树的宽度,递归的深度就构成了树的深度。
回溯三部曲:
1.递归函数参数和返回值
2.确定终止条件
3.单层递归的逻辑
循环的时候嵌套递归,深度就是递归的层数(和终止条件有关,在这道题中和k有关系)。循环到每一个数时候,都要递归下去。
一、组合问题(回溯法)
给定两个整数 n
和 k
,返回范围 [1, n]
中所有可能的 k
个数的组合。
你可以按 任何顺序 返回答案。
三部曲:
1.返回值为void,参数为:int n,int k,startIndex。n和k是题目给的条件,startIndex是每次递归进去for循环的起始位置。
2.终止条件:当集合的size()=k的时候,就要收割结果了。
3.单层递归逻辑:
for(int i=startIndex;i<=n;i++){
list.add(i);
backtracing(n,k,i+1);
list.removelast();
}
代码:
class Solution {
private List<List<Integer>> lists=new ArrayList<>();
private List<Integer> list=new ArrayList<>();
public List<List<Integer>> combine(int n, int k) {
backTracing(n,k,1);
return lists;
}
public void backTracing(int n,int k,int startIndex){
//收割结果 如果list集合的大小==k 就加到集合里面
if(list.size()==k){
lists.add(new ArrayList(list));
return;
}
for(int i=startIndex;i<=n;i++){
list.add(i);
backTracing(n,k,i+1);
list.removeLast();
}
}
}
剪枝:
k-list.size():还需要的元素。n-(k-list.size())+1;代表的是在该list.size()的情况下,至多从哪个元素开始刚好可以遍历完k个元素。
k-list.size()还需要元素的个数。一共有n个元素,我需要k-list.size()。我就需要从n-(k-list.size())+1开始,如果比这个还要大的话,就肯定是不够的。
也就是说n-(k-list.size())+1,是每一行右边的边界。
二、组合总和III(回溯法)
找出所有相加之和为 n
的 k
个数的组合,且满足下列条件:
- 只使用数字1到9
- 每个数字 最多使用一次
返回 所有可能的有效组合的列表 。该列表不能包含相同的组合两次,组合可以以任何顺序返回。
1->9里面 k个数 加起来和为n
仍然使用回溯法+剪枝操作:
全局变量:List<Integer> path List<List<Integer>> paths
1.返回值:void 参数:int target(n),int k,int startIndex
2.终止条件:当path.size()==k&&sumOfList(path)==n 把集合放大paths中
剪枝条件:当sumOfList(path)>n return;
3.单层递归逻辑:for(int i=startIndex;i<=9-(k-path.size())+1;i++)
循环里面递归
代码:
class Solution {
private List<Integer> path = new ArrayList<>();
private List<List<Integer>> paths = new ArrayList<>();
public List<List<Integer>> combinationSum3(int k, int n) {
backTracing(k, n, 1);
return paths;
}
public void backTracing(int k, int n, int startIndex) {
if (sumOfList(path) > n)
return;
if (path.size() == k && sumOfList(path) == n) {
paths.add(new ArrayList<>(path));
}
for (int i = startIndex; i <= 9-(k-path.size())+1; i++) {
path.add(i);
backTracing(k, n, i + 1);
path.removeLast();
}
}
public int sumOfList(List<Integer> list) {
int sum = 0;
for (int i : list) {
sum += i;
}
return sum;
}
}
三、电话号码的字母组合
给定一个仅包含数字 2-9
的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
输入:digits = "23" 字符串 输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]
输入2:abc 输入3:def
ad\ae\af\bd\.....
思路:digits的length()就是递归的深度。以前两道题递归的深度是k的大小。
递归的广度是每一个按键上字母的个数,
1.返回值为void 参数为:String digits(代表23),int index(代表递归到多深了)
2.终止条件(index==digits.length()) 将path添加到result集合中
3.单层递归逻辑:
3.1首先根据index,然后从digits中获取到数字,根据数字在字符串letterMap中获取对应的字符串(abc/def...)
3.2获取到对应的字符串之后,使用for循环进行遍历,取第i个字母,然后继续递归到下一层。
3.3直到递归到index==digits.length();
代码:
class Solution {
String[] letterMap = { "", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz" };
List<String> result=new ArrayList<>();// 存放结果集
String path=new String();// 单次的结果
StringBuilder sb=new StringBuilder();// 拼接字符串的工具
public List<String> letterCombinations(String digits) {
if(digits==null||digits.length()==0)
return result;
backTracing(digits,0);
return result;
}
public void backTracing(String digits, int index) {
// 终止条件
if(index==digits.length()){
result.add(new String(sb));
return;
}
//找字符串
Integer number=digits.charAt(index)-'0';
String str=letterMap[number];
//for循环遍历递归
for(int i=0;i<str.length();i++){
sb.append(str.charAt(i));
backTracing(digits,index+1);
sb.deleteCharAt(sb.length()-1);
}
}
}
四、组合总和
给定一个无重复元素的正整数数组 candidates
和一个正整数 target
,找出 candidates
中所有可以使数字和为目标数 target
的唯一组合。candidates
中的数字可以无限使用。
遇到的问题:2355 3255 是一样的组合,要把这种重复的组合去掉
思路:和之前题目一样,终止条件和单层递归逻辑有些区别
2.终止条件:
当sumOfList(path)==target 把path添加到result中,return;
当sumOfList(path)>target rerun; 否则会无限递归下去
3.单层递归逻辑:for循环 i=startIndex i<candidates.length i++
先进行剪枝判断,然后加进去,最后回溯
代码:
class Solution {
private List<List<Integer>> result = new ArrayList<>();
private List<Integer> path = new ArrayList<>();
public List<List<Integer>> combinationSum(int[] candidates, int target) {
backTracing(candidates, target, 0);
return result;
}
public void backTracing(int[] candidates, int target, int startIndex) {
if (sumOfList(path) > target)
return;
if (sumOfList(path) == target) {
result.add(new ArrayList<>(path));
return;
}
for (int i = startIndex; i < candidates.length; i++) {
if(sumOfList(path)+candidates[i]>target)break;
path.add(candidates[i]);
backTracing(candidates, target, i);
path.removeLast();
}
}
public int sumOfList(List<Integer> list) {
int sum = 0;
for (Integer i : list) {
sum += i;
}
return sum;
}
}
五、组合总和II
和上一道题不一样的地方是这里的元素可能是重复的。给定一个可能有重复数字的整数数组 candidates
和一个目标数 target
,找出 candidates
中所有可以使数字和为 target
的组合。eg:如果candidates数组里面有1 1 2。target为:3。这样就会出现两个1,2
注意:一定要有去重操作:当i>startIndex,并且遇到candidates[i]==candidates[i-1],在下标为i-1的时候,已经遇到过所有的情况,在下标为i的时候就不需要考虑了。因此直接跳过continue就行。
代码:
class Solution {
List<List<Integer>> result = new ArrayList<>();
List<Integer> path = new ArrayList<>();
public List<List<Integer>> combinationSum2(int[] candidates, int target){
if(candidates.length==0)return result;
Arrays.sort(candidates);
backTracings(candidates,target,0);
return result;
}
public void backTracings(int[] candidates, int target, int startIndex){
//终止条件
if(sumOfList(path)>target)return;
if(sumOfList(path)==target){
result.add(new ArrayList<>(path));
return;
}
//单层递归逻辑
for(int i=startIndex;i<candidates.length;i++){
//剪枝
if(candidates[i]+sumOfList(path)>target)return;
//去重代码
if(i>startIndex&&candidates[i]==candidates[i-1])continue;
//普通回溯
path.add(candidates[i]);
backTracings(candidates,target,i+1);
path.removeLast();
}
}
public int sumOfList(List<Integer> list) {
int sum = 0;
for (Integer i : list) {
sum += i;
}
return sum;
}
}
使用boolean[] flag 标记的那种方法是:if(i>0&&candidates[i]==candidaes[i-1]&&flag[i]!=1)
这种情况因为是i>0 对i没有限制
不使用标记的方法是:if(i>startIndex&&candidates[i]==candidaes[i-1]) 要求i>startIndex,在第一遍的时候都不会满足i>startIndex,eg:1,1,2 在第一个1的时候 不会满足这个条件的.
六、分割回文串:
复习:String.substring(int start,int end);包头不包尾
问题:终止条件||哪段字符串需要进行判断||将集合转换成string字符串
思路:和平常的回溯题一样,水平方向是分割字符串的方法,可以分第一个,也可以直接分两个,也可以直接分三个。这个体现在for循环中(int i=startIndex;i<s.length();i++),切割的范围为(startIndex,i);当for循环进行第二次遍历的时候,就是以以前两个为整体切割的。
1.递归函数的返回值:void 参数:String s,int startIndex
2.终止条件:当startIndex到达s.length()的时候,就说明到了叶子节点,后面没有元素可以添加进去,这时候我们就把分割好的字符串加到Result中。(判断是否是回文串在单层递归逻辑中实现)
3.单层递归逻辑:for(int i=startIndex,i<s.length();i++)
然后切割的字符串就是[startIndex,i];如果是就加入到list中,如果不是就continue;
代码:
class Solution {
List<String> list=new ArrayList<>();
List<List<String>> result=new ArrayList<>();
public String[][] partition(String s) {
backTracing(s,0);
String[][] res=new String[result.size()][];
for(int i=0;i<res.length;i++){
List<String> path=result.get(i);
res[i]=path.toArray(new String[path.size()]);
}
return res;
}
public void backTracing(String s,int startIndex){
//终止条件:如果切割到叶子节点说明 分割到底 并且都是回文串
if(startIndex>=s.length()){
result.add(new ArrayList<>(list));
return;
}
//单层遍历逻辑
for(int i=startIndex;i<s.length();i++){
boolean flag=isPalindrome(s,startIndex,i);//判断是否是回文串
if(flag){
list.add(s.substring(startIndex,i+1));//含头不含尾
}else{
continue;
}
backTracing(s,i+1);
list.remove(list.size()-1);
}
}
public boolean isPalindrome(String s,int left,int right){
for(int i=left,j=right;i<j;i++,j--){
if(s.charAt(i)!=s.charAt(j))return false;
}
return true;
}
}