本篇博客主要讲解一下回溯算法的典型例题。为什么我没有选力扣里的组合总和Ⅰ、Ⅲ或Ⅳ呢?因为除了Ⅳ其他三题都是回溯算法,三题相差不大,Ⅳ是一道动态规划的题我们后面再说。
题目
题目链接:
给定一个数组 candidates
和一个目标数 target
,找出 candidates
中所有可以使数字和为 target
的组合。
candidates
中的每个数字在每个组合中只能使用一次。
注意:解集不能包含重复的组合。
示例 1:
输入: candidates = [10,1,2,7,6,1,5], target = 8,
输出:
[
[1,1,6],
[1,2,5],
[1,7],
[2,6]
]
示例 2:
输入: candidates = [2,5,2,1,2], target = 5,
输出:
[
[1,2,2],
[5]
]
提示:
1 <= candidates.length <= 100
1 <= candidates[i] <= 50
1 <= target <= 30
解题思路
先看题目,有几个要求:
- 找到和为
target
的数字组合 - 数组
candidates
中的每个数字只能使用一次(每一个组合中只能用一次) - 解集不能包含重复组合
然后根据要求可以想到需要有一个标记的数组(我用f
表示),其长度应该和数组candidates
的长度一致。需要返回一个二维的vector
容器(num
),关于解集去重我们先不想,先想如何找到和为target
的数组组合。
这类题型应该可以很容易看出可以用回溯求解,因为数组中的每个元素都要遍历取值相加,发现不符合题意就回溯和还原。那么知道用回溯求解就好办了:
首先要想的是递归的函数要有哪些参数?需要遍历candidates
数组,肯定要有该数组。既然是要求和,一定要一个参数放置累加的和(m
)。
然后要想的是什么条件下返回(回溯),由题目可知当m
等于target
时,满足题意,将所存储的组合数放入需要返回的数组num
中。当m
大于等于target
时,如果继续递归下去或者说继续累加下去永远不可能得到目标数,所以此时应该回溯。
由于返回的是二维的,所以可以用一个一维(ber)先存储每次遍历的组合,当符合条件时再将其放入二维里
再然后应该想的是递归函数里需要做什么事情,这里可以用一个例子或一个数据带入想,因为在每次递归里所做的事都是一样的,对一个例子成立对其他例子也一样成立。所以就拿示例一讲吧!当第一次进入递归时,我们要对这个candidates
遍历,因为每个元素都需要和其他元素进行相加才能找到每个正确答案,这里就要一个for循环。每次遍历都需要将遍历的数加起来,同时也要将遍历的数放入一维容器里。这时这一层递归就基本结束了,因为要做的事已经做了,可以进入下一层递归了。一层递归结束后,我们需要把之前更改的变量值都还原。就像每次吃完饭都要把碗洗干净,这样下次吃饭的时候可以和这次吃饭的时候使用同一个碗。伪代码如下:
for (int i = 0; i < candidates.size(); ++ i) {
if (这个数没有使用过) {
累加i;
将i放入容器中;
还要将判断这个数没有使用过的标记数组置为true;
//此时要做的事做完了
dfs(candidates, target, 累加的数);
//还原
将累加的数减去i;
将容器的i弹出;
将标记数组置false;
}
}
至此,主体已经完成的差不多了,可以去解决组合总和,那题和这题的区别主要在这题多了去重,我们看看去重的部分:
开始我没想到好的方法,我想遍历一遍返回数组num
检查要放入的数组ber
是否在num
中已经存在,后面我放弃了。。。
然后我打开评论区,妙啊!这里我来讲解一下。我们可以先用sort函数将需要遍历的数组candidates
排序,然后相同项就会放在一起。我们以实例一为例:当遍历第一个相同项1时,我们可以使用后面的相同项。但是当遍历到第二个相同项1时,如果不加任何措施会出现两个[1,7]和[1,2,5]数组,这时可以看到凡是1单独出现的数组都会重复,所以加一个判断条件,当candidates[i] == candidates[i - 1]
时,直接跳过。但还有个细节的是,数组[1,1,6]没法输出了,那就再加个条件:!f[i - 1]
。如果还不清楚可以看代码理解。
代码(c++)
class Solution {
public:
vector<vector<int>> num;
vector<int> ber;
bool f[105];
void dfs(vector<int> candidates, int target, int n, int m) {
if (m > target)
return ;
else if (m == target) {
num.push_back(ber);
return ;
}
for (int i = n; i < candidates.size(); ++ i) {
if (i > 0 && candidates[i] == candidates[i - 1] && !f[i - 1]) //去重
continue;
if (!f[i]) {
ber.push_back(candidates[i]);
f[i] = true;
dfs(candidates, target, i + 1, m + candidates[i]);
ber.pop_back();
f[i] = false;
}
}
}
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
if (candidates.size() == 0)
return num;
sort(candidates.begin(), candidates.end());
dfs(candidates, target, 0, 0);
return num;
}
};
总结
对于组合总和Ⅲ,我觉得可以举一反三,这题懂了那题也很容易。递归回溯这类题,其实多写一点就熟练了,你会感觉就像模板一样,拿到题直接套就行。