LeetCode 组合、组合总和(I ~ IV)(C++)(回溯法、图的深度优先遍历)
原创
©著作权归作者所有:来自51CTO博客作者wx63a03d571f3d9的原创作品,请联系作者获取转载授权,否则将追究法律责任
1、组合
问题描述
给定两个整数n
和k
,返回1 ... n
中所有可能的 k
个数的组合。
示例:
输入: n = 4, k = 2
输出:
[
[2,4],
[3,4],
[2,3],
[1,2],
[1,3],
[1,4],
]
解题思路
- 该题目使用 回溯(su)法也等价于树(图)的深度优先遍历算法来求解。
- 分别定义用于存储最终结果的二维
vector
容器和存储每个结点的一维vector
容器。二维容器用于将每次一维容器装满元素后的压入。而大小为k
的一维容器用于装大小为k
的个数的组合。当数量满足为k
的时候,压入到二维容器中。 - 核心思想是图的深度优先遍历。当一维容器大小满足
k
个时,需要结点回退,重新将下一个新的元素压入一维容器中。再加上for
循环,可以保证遍历到所有符合情况且不重复的元素。 - 考虑到
k
值问题,在遍历过程中,n
个结点剩余小于k
个结点没有遍历时,可以停止遍历,因为此时已不满足k
个元素成为一个新的结点压入到二维数组中。这是一种剪枝的思想。
代码实现(1)
class Solution {
public:
vector< vector<int> > res; // 存放符合条件结果的集合
vector<int> path; // 用来存放符合条件结果
void backtracking(int startIndex, int n, int k) {
if (path.size() == k) {
res.push_back(path);
return;
}
// 这个for循环有讲究,组合的时候 要用startIndex,排列的时候就要从0开始
for (int i = startIndex; i <= n - (k - path.size()) + 1; i++) {
path.push_back(i); // 处理节点
backtracking(i + 1, n, k);
path.pop_back(); // 回溯,撤销处理的节点
}
}
vector<vector<int>> combine(int n, int k) {
backtracking(1, n, k);
return res;
}
};
运行截图
代码实现(2)
class Solution {
public:
vector< vector<int> > ans;
vector<int> temp;
void deepFirstTraverse(int index, int n, int k){
if(temp.size() == k){
ans.push_back(temp);
return;
}
if(temp.size() + (n - index + 1) < k)
return;
temp.push_back(index);
deepFirstTraverse(index + 1, n, k);
temp.pop_back();
deepFirstTraverse(index + 1, n, k);
}
vector<vector<int>> combine(int n, int k) {
deepFirstTraverse(1, n, k);
return ans;
}
};
运行截图
2、组合综合
问题描述
给定一个无重复元素的数组 candidates
和一个目标数 target
,找出 candidates
中所有可以使数字和为target
的组合。
candidates
中的数字可以无限制重复被选取。
说明:
所有数字(包括 target)都是正整数。
解集不能包含重复的组合。
示例 1:
输入:candidates = [2,3,6,7], target = 7,
所求解集为:
[
[7],
[2,2,3]
]
示例 2:
输入:candidates = [2,3,5], target = 8,
所求解集为:
[
[2,2,2,2],
[2,3,3],
[3,5]
]
提示:
1 <= candidates.length <= 30
1 <= candidates[i] <= 200
candidate 中的每个元素都是独一无二的。
1 <= target <= 500
解题思路
- 核心思想是回溯法(图的深度优先遍历)。
- 本题目递归函数与上有题目有所区别:由于元素的可重复性利用,每次递归函数中的参数则不发生变化。例如:中找到加和为的元素。首先元素符合题意,压入容器。进入递归函数,在同样的数组中,此时加和为。
- 可以使用剪枝技术,但事先需要对元素进行排序。
代码实现
class Solution {
public:
vector< vector<int> > ans;
vector<int> temp;
void DeepFirstTraverse(int index, vector<int>& candidates, int target){
int acc = accumulate(temp.begin(), temp.end(), 0);
if(acc == target){
ans.push_back(temp);
return;
}
for(int i = index; i < candidates.size(); i++){
if(target - acc >= candidates[i]){ // 剪枝
temp.push_back(candidates[i]);
DeepFirstTraverse(i, candidates, target);
temp.pop_back(); // 回溯
}
else
break;
}
}
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
sort(candidates.begin(), candidates.end());
DeepFirstTraverse(0, candidates, target);
return ans;
}
};
运行截图
3、组合总和 II
问题描述
给定一个数组 candidates
和一个目标数 target
,找出 candidates
中所有可以使数字和为 target
的组合。
candidates
中的每个数字在每个组合中只能使用一次。
说明:
所有数字(包括目标数)都是正整数。
解集不能包含重复的组合。
示例 1:
输入: candidates = [10,1,2,7,6,1,5], target = 8,
所求解集为:
[
[1, 7],
[1, 2, 5],
[2, 6],
[1, 1, 6]
]
示例 2:
输入: candidates = [2,5,2,1,2], target = 5,
所求解集为:
[
[1,2,2],
[5]
]
解题思路
- 与上题目不同的是:本题目中的元素是可以重复出现的并且计算加和的过程同一个元素不可重复利用。
- 基本思想还是递归加回溯。
- 但是本题目在处理相同结点的时候,需要进一步考虑。可以使用容器的去重方法和下面代码中的剪枝2,在原数组中进行去重。
代码实现
class Solution {
public:
vector< vector<int> > ans;
vector<int> temp;
void dfs(int index, vector<int>& candidates, int target){
int acc = accumulate(temp.begin(), temp.end(), 0);
if(acc == target){
auto it=ans.begin();
it=find(ans.begin(),ans.end(),temp);
if(it==ans.end())
ans.push_back(temp);
}
for(int i = index; i < candidates.size(); i++){
if(target - acc >= candidates[i]){
temp.push_back(candidates[i]);
dfs(i + 1, candidates, target);
temp.pop_back();
}
else
break;
}
}
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
sort(candidates.begin(), candidates.end());
dfs(0, candidates, target);
//sort(ans.begin(), ans.end());
//vector< vector<int> >::iterator iter;
//iter = unique(ans.begin(), ans.end());
//ans.erase(iter, ans.end());
return ans;
}
};
运行截图
代码实现(2)
- 为防止结果出现重复元素,对原数组中的连续元素进行剪枝。
class Solution {
vector<int> nums;
vector<vector<int>> ans;
vector<int> a;
public:
vector<vector<int>> combinationSum2(vector<int>& nums, int target) {
sort(nums.begin(), nums.end());
this->nums = move(nums);
dfs(0, target);
return ans;
}
void dfs(int l, int target) {
if (target == 0) {
ans.push_back(a);
return;
}
for (int i = l; i < nums.size(); ++i) {
if (target - nums[i] < 0) break; //剪枝1
if (i > l && nums[i] == nums[i - 1]) continue; //剪枝2
a.push_back(nums[i]);
dfs(i + 1, target - nums[i]);
a.pop_back();
}
}
};
4、组合总和 III
问题描述
找出所有相加之和为 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]]
解题思路
- 使用递归加回溯。
- 自定义
0-9
的数组用过初始数组。定义二维容器和一维容器,判断条件就是以为容器元素和为n
并其长度为k
。
代码实现
class Solution {
public:
vector< vector<int> > ans;
vector<int> temp;
void dfs(int index, vector<int>& a, int k, int n){
int acc = accumulate(temp.begin(), temp.end(), 0);
if(n == acc and temp.size() == k){
ans.push_back(temp);
return;
}
for(int i = index; i < a.size(); i++){
if(n - acc >= 0 and temp.size() <= k){
temp.push_back(a[i]);
dfs(i + 1, a, k, n);
temp.pop_back();
}
else
break;
}
}
vector<vector<int>> combinationSum3(int k, int n) {
vector<int> a = {1, 2, 3, 4, 5, 6, 7, 8, 9};
dfs(0, a, k, n);
return ans;
}
};
运行截图
5、组合总和 Ⅳ
问题描述
给定一个由正整数组成且不存在重复数字的数组,找出和为给定目标正整数的组合的个数。
示例:
nums = [1, 2, 3]
target = 4
所有可能的组合为:
(1, 1, 1, 1)
(1, 1, 2)
(1, 2, 1)
(1, 3)
(2, 1, 1)
(2, 2)
(3, 1)
请注意,顺序不同的序列被视作不同的组合。
因此输出为 7。
进阶:
如果给定的数组中含有负数会怎么样?
问题会产生什么变化?
我们需要在题目中添加什么限制来允许负数的出现?
解题思路
- 本题目使用动态规划算法来求解。
- 定义状态为总和为的所有可能的方法数。那么将所有方法按照最后一个数字进行分类,就可以得到状态转移方程:
如果,。
代码实现
class Solution {
public:
int combinationSum4(vector<int>& nums, int target) {
vector<unsigned long long> dp(target + 1, 0);
dp[0] = 1;
for(int i = 0; i <= target; i++){
for(int j = 0; j < nums.size(); j++){
if(i - nums[j] >= 0)
dp[i] += dp[i - nums[j]];
}
}
return dp[target];
}
};
运行截图
总结
- 回溯法在算法领域应用很广,等价于图的深度优先遍历算法。