一、题目描述:
leetcode 77: 给定n和k,返回[1,2...n]中子集的集合,这些子集的元素个数为k,不限制顺序。
二、思路分析:
1.类别
递归、回溯
2.做题思路
因此可以写出递归函数是void recurAns(int l,int r,int k,vector<int> &path,vector<vector<int>> &ans),l和r表示区间的左和右界,path是递归求出来的单个满足条件的子集,ans是最后返回的集合。返回的边界条件有两个:当前的子集path的数量等于k以及左界大于右界。对于一个元素,考虑两种情况:
- 元素不在当前子集里:
- 不添加元素(无操作)
- 剩余部分:
recurAns(l+1,r,k,path,ans);
- 元素在当前子集里:
- 添加元素进入path:
path.push_back(l); - 剩余部分:
recurAns(l+1,r,k,path,ans); - 恢复现场(回溯):
path.pop_back();由于最后的结果由边界条件产生,也就是当path的元素个数为k个时,ans.push_back(path);这一步,因此可以直接把这两个操作并列写下来,因为最后的结果都将被加入ans。 具体代码如下:
void recurAns(int l,int r,int k,vector<int> &path,vector<vector<int>> &ans) {
if(path.size()==k){
ans.push_back(path);
}
if(l>r)
return;
recurAns(l+1,r,k,path,ans);
path.push_back(l);
recurAns(l+1,r,k,path,ans);
path.pop_back();
}
但这样会产生重复项,因为回溯的时候会在这一边界条件后再次调用自己。因此应该加入ans后直接跳出函数。
void recurAns(int l,int r,int k,vector<int> &path,vector<vector<int>> &ans) {
if(path.size()==k){
ans.push_back(path);
return; //避免回溯带来的重复
}
if(l>r)
return;
recurAns(l+1,r,k,path,ans);
path.push_back(l);
recurAns(l+1,r,k,path,ans);
path.pop_back();
}
主函数只需要传入整个1到n数组的左界和右界,给定的k和空集path与空集ans即可。
vector<vector<int>> combine(int n, int k)
{
vector<vector<int>> ans;
vector<int> path;
recurAns(1,n,k,path,ans);
return ans;
}
};
三、AC 代码:
class Solution {
public:
void recurAns(int l,int r,int k,vector<int> &path,vector<vector<int>> &ans) {
if(path.size()==k){
ans.push_back(path);
return;
}
if(l>r)
return;
recurAns(l+1,r,k,path,ans);
path.push_back(l);
recurAns(l+1,r,k,path,ans);
path.pop_back();
}
vector<vector<int>> combine(int n, int k)
{
vector<vector<int>> ans;
vector<int> path;
recurAns(1,n,k,path,ans);
return ans;
}
};
四、总结:
1.这题虽然写的很快但是递归不够优雅,会出现重复的项,主要是因为回溯的时候重复在ans中加入了相同的path。网上应该还有更好的回溯版本,这里只是一个参考的解法。关于这种解法的问题会在leetcode 78.Subsets【递归】【medium】中有更详细的解释。
- 关于vector和set的转换 对于重复项,另一个思路是可以使用C++ STL中的set进行去重,由于主函数返回的要求是vector,我们可以在主函数中先把vector转化成set,这样就能够自动去重,然后再把set中的内容赋值到ans中,具体代码如下:
set<vector<int>> st(ans.begin(), ans.end());
ans.assign(st.begin(), st.end());
本文正在参与「掘金 2021 春招闯关活动」, 点击查看 活动详情