1. 组合
回溯法又叫回溯搜索法,是一种搜索算法.回溯是递归的副产品.回溯算法的本质是穷举,虽然可以有剪枝操作,但本质上还是穷举,效率不高.一些问题由于没有别的解法,所以我们只能使用回溯算法
回溯法解决的问题可以抽象为树形结构,集合的大小决定了数的宽度(分支数),递归的深度决定了树的深度(例如k个元素的子集,k就是递归深度)
77.
给定两个整数 n 和 k,返回 1 … n 中所有可能的 k 个数的组合。
示例:
输入: n = 4, k = 2
输出:
[
[2,4],
[3,4],
[2,3],
[1,2],
[1,3],
[1,4],
]
1. 题解
本题首先要画递归树,整体上了解递归树的遍历顺序(细节可以不用扣太细,可能会把自己搞晕),然后将具体问题代入模板即可.关键:画出递归树
递归树的统一画法: 叶子节点为可选集合,分支为选择集合中的某一个元素.递归的顺序为整体上从左到右(所有子节点间),局部从上到下(单个节点).
2. 代码
private List<Integer> ipath=new ArrayList<Integer>();
private List<List<Integer>> ires=new ArrayList<List<Integer>>();
private List<Character> cpath=new ArrayList<Character>();
private List<List<Character>> cres=new ArrayList<List<Character>>();
public List<List<Integer>> combine(int n, int k) {
tracebackingOfcombine(n,k,1);
return ires;
}
public void tracebackingOfcombine(int n,int k,int startIndex){
if (ipath.size()==k){
ires.add(new ArrayList<>(ipath));
return;
}
for (int i = startIndex; i <=n; i++) {
ipath.add(i);
tracebackingOfcombine(n,k,i+1);
ipath.remove(ipath.size()-1);
}
}
3. 剪枝
当当前数组长度(递归宽度)+当前path数组内的元素<k时,数组长度不可能到达,可以剪枝
/ 组合+剪枝
public void tracebackingOfcombine02(int n,int k,int startIndex){
if (ipath.size()==k){
ires.add(new ArrayList<>(ipath));
return;
}
for (int i = startIndex; i <=n; i++) {
if (ipath.size()+n-startIndex+1<k){
return;
}
ipath.add(i);
tracebackingOfcombine(n,k,i+1);
ipath.remove(ipath.size()-1);
}
}
2.组合求和
力扣题目链接(opens new window)
找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。
说明:
所有数字都是正整数。
解集不能包含重复的组合。
示例 1: 输入: k = 3, n = 7 输出: [[1,2,4]]
示例 2: 输入: k = 3, n = 9 输出: [[1,2,6], [1,3,5], [2,3,4]]
1.思路
本题为组合问题,初始数组[1,…,9],递归深度 k, 终止条件 (path.size==k&&sum==n),初始值 startIndex=1,剪枝条件(组合传统剪枝+sum>=n)
2. 代码
private List<Integer> ipath=new ArrayList<Integer>();
private List<List<Integer>> ires=new ArrayList<List<Integer>>()
// 9. 组合总和
public List<List<Integer>> combinationSum3(int k, int n) {
combinationSum3trancebacking(n,1,k,0);
return ires;
}
public void combinationSum3trancebacking(int n,int startIndex,int k,int sum){
if (ipath.size()==k&&sum==n){
ires.add(new ArrayList<>(ipath));
return;
}
for (int i = startIndex; i <=9; i++) {
if (ipath.size()+9-startIndex+1<k&&sum>n){
return;
}
ipath.add(i);
sum+=i;
combinationSum3trancebacking(n,i+1,k,sum);
//回溯
ipath.remove(ipath.size()-1);
sum-=i;
}
}
3. 上述代码有什么缺点?
== 深度剪枝==
if (ipath.size()==k&&sum==n){
ires.add(new ArrayList<>(ipath));
return;
}
修改
在这里插入代码片
这段代码,当path的长度到达k,但是sum没有达到标准,会继续向深处执行,并没有达到在第k层就分会的目的.
修改
if (ipath.size()==k){
if(sum==n){
ires.add(new ArrayList<>(ipath));
}
return;
}
广度剪枝
if (ipath.size()+9-startIndex+1<k&&sum>n){
return;
}
这段代码应该是|| 才能最大化的剪枝
4. 特殊情况:求两个值的和时
先排序,然后使用双指针
// 法二 双指针法
public int[] twoSum(int[] nums, int target) {
int p=0,q=nums.length-1;
while (p<q){
if (nums[p]+nums[q]>target){
q--;
}else if (nums[p]+nums[q]<target){
p++;
}else {
break;
}
}
return new int[]{nums[p],nums[q]};
}
3. 组合求和(数组元素可以被重复选取)
1. 思路
终止条件: path和==总和
剪枝:
本题相较与2.组合总和剪枝策略有改进,
上一题中,
if (ipath.size()+9-startIndex+1<k&&sum>n){
return;
}
是当前节点的父节点的sum>n,直接当前节点直接不用加入和递归其子节点了,直接返回
而本题
if (sum+candidates[i]>target){
continue;// 直接剪掉一个不可能的分支,从其兄弟节点进行排查
}
尝试加入当前节点,如果大于sum,则直接放弃该节点,去兄弟节点尝试.
2. 代码 (标准形式)
//11. 组合总和+重复选取
public List<List<Integer>> combinationSum(int[] candidates, int target) {
combinationSumTracebacking(candidates,target,0,0);
return ires;
}
public void combinationSumTracebacking(int[] candidates, int target,int sum,int startIndex) {
if (sum==target){
ires.add(new ArrayList<>(ipath));
return;
}
for (int i = startIndex; i <candidates.length ; i++) {
if (sum+candidates[i]>target){
continue;// 直接剪掉一个不可能的分支,从其兄弟节点进行排查
}
ipath.add(candidates[i]);
sum+=candidates[i];
combinationSumTracebacking(candidates,target,sum,i);
ipath.remove(ipath.size()-1);
sum-=candidates[i];
}
}
4. 数组之间的组合
17.电话号码的字母组合
力扣题目链接(opens new window)
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
17.电话号码的字母组合
示例: 输入:“23” 输出:[“ad”, “ae”, “af”, “bd”, “be”, “bf”, “cd”, “ce”, “cf”].
说明:尽管上面的答案是按字典序排列的,但是你可以任意选择答案输出的顺序。
1. 题解
2.代码
// 10. 数组之间的组合
private List<String> res=new ArrayList<>();
public List<String> letterCombinations(String digits) {
if (digits==null||digits.length()==0){
return res;
}
letterCombinationsteacebacking(digits,0,new StringBuilder());
return res;
}
public void letterCombinationsteacebacking(String digits,int startIndex,StringBuilder path) {
if (path.length()==digits.length()){
res.add(new String(path));
return;
}
ArrayList<Character> chars = getStrings(digits.charAt(startIndex));
for (int j = 0; j <chars.size() ; j++) {
path.append(chars.get(j));
letterCombinationsteacebacking(digits,startIndex+1,path);
path.deleteCharAt(path.length()-1);
}
}
private ArrayList<Character> getStrings(char ch) {
ArrayList<Character> chars = new ArrayList<>();
if (ch-'0'==0){
chars.add('!');
chars.add('@');
chars.add('#');
}
if (2<=ch-'0'&&ch-'0'<=7){
chars.add((char) ('a'+(ch-'0'-2)*3));
chars.add((char) ('a'+(ch-'0'-2)*3+1));
chars.add((char) ('a'+(ch-'0'-2)*3+2));
if (ch-'0'==7){
chars.add('s');
}
}
if (ch-'0'==8){
chars.add('t');
chars.add('u');
chars.add('v');
}
if (ch-'0'==9){
chars.add('w');
chars.add('x');
chars.add('y');
chars.add('z');
}
return chars;
}
5.总结
组合问题的关键是画出递归图,然后代入模板,观察题目特性,生成剪枝条件.在递归出口处的剪枝条件可以横向剪枝(剪掉若干层),在循环里面的剪枝可以纵向剪枝,剪 掉一棵树.当遇到组合问题的时候,可以往本文讲的几道问题上靠拢.