本文参考:carl大佬–代码随想录的题解
个人算法小抄:
1.组合问题,无序性。如{2,4}和{4,2}是同一种结果。考虑index控制。
2.排序问题,有序性。如{1,2,3}和{3,2,1}是不一样的。考虑维护used数组。
3.分割问题,虚拟出一条分割线。很多时候直接在原数据上操作及撤回。
4.子集问题,当用三部曲模板时(其实就是迭代加递归)。搜集全部经过的节点。 当使用双递归时,收集根节点即可。
样例:
输入: n = 4, k = 2
输出:
[
[2,4],
[3,4],
[2,3],
[1,2],
[1,3],
[1,4],
]
总结:
遇到这种,不能颠倒顺序的组合问题(如只能[1,2],不能[2,1])的时候,在递归中传入一个start,作为for横向选择时的脚标,避免走回头路。
坑点:
下一层递归是从i+1开始的,不然会重复选择相同的数字本身。(如[2,2])。 如果记不住,想想[1]的时候咋选[3],我这时候i指向2,下一层肯定是i+1才能是3呀。如果是start加一岂不是又选到2了。
剪枝:
存在一种情况如n = 4,k = 4的时候,如果第一层不选1,从2开始选,那么这条树枝全部都是没有意义的,因为后面的元素全选了都不够满足条件。
因此,可以约束i的上界条件:
1.当前已选择数量为 path.size();
2.需求元素的数量自然就是 k - path.size();
3.所以在Nums数组中,只有在某个脚标之前的选择才是有意义的-- nums.size() - (k - path.size()) + 1。至于为啥要加一,可以把path.size() = 0带进入一个具体的例子如n = 4 ,k = 3中。
class Solution {
public:
vector<vector<int>> ans;
vector<int> path;
vector<vector<int>> combine(int n, int k) {
vector<int> nums(n);
for (int i = 0; i < n; i++)
nums[i] = i + 1;
backtrack(nums, k, 0);
return ans;
}
void backtrack(vector<int>& nums,int k,int start)
{
if (path.size() == k)
{
ans.push_back(path);
return;
}
//已选path.size(),需求 k - path.size(),只有n - (需求)+1位置之前的有意义 (之后的元素全选都不够了,直接剪枝)
for (int i = start; i < nums.size() - (k - path.size()) + 1; ++i) //剪枝
{
int a = nums[i];
path.push_back(nums[i]);
backtrack(nums, k, i + 1);//注意下一层要从i+1Kaishi
path.pop_back();
}
}
};
这题可以用交换来改变排列的。撤销操作就是再交换一遍。 用一个level(level)参数来影响循环中的i的值,实现不同的交换对象。但排列问题有更通用的模板
class Solution {
public:
vector<vector<int>> ans;
vector<vector<int>> permute(vector<int>& nums) {
if (nums.empty())
return ans;
dfs(nums, 0);
return ans;
}
void dfs(vector<int>& nums, int level)
{
if (level == nums.size())
{
ans.push_back(nums);
return;
}
for (int i = level; i < nums.size(); ++i)
{
swap(nums[i], nums[level]);
dfs(nums, level+1);
swap(nums[i], nums[level]);
}
}
};
class Solution {
public:
vector<vector<int>> ans;
vector<vector<int>> permute(vector<int>& nums) {
if (nums.empty())
return ans;
vector<bool> used(nums.size());
dfs(nums, used);
return ans;
}
vector<int> path;
void dfs(vector<int>& nums, vector<bool>& used)
{
if (path.size() == nums.size())
{
ans.push_back(path);
return;
}
for (int i = 0; i < nums.size(); i++)
{
if (used[i] == true) continue; //跳过那些已经访问过的
used[i] = true; //标记当前访问
path.push_back(nums[i]);
dfs(nums, used);
used[i] = false; //撤销访问操作
path.pop_back();
}
}
};
下面是一个基于46题排列问题的剪枝问题,这个剪枝方法也很常用。
样例:
输入:
nums = [1,1,2]
输出:
[[1,1,2],
[1,2,1],
[2,1,1]]
这题和46的区别就是给定数组中有重复数字需要剪枝去重。
注意首先要对给定数组排序,这样才可以让相同的数挨到一起。
去重核心代码:
当前这个数和前一个数重复了,并且站在这一层观察以往的used情况,发现前一个重复的数竟然没有被用到。说明了啥,说明了这个重复的数字在本层中被用到了,换句话说就是这个重复的数字和“我”占据了同一个位置。那么“我”这条树枝,就要全部被剪枝。
if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) {
continue;
}
此外还有一种树枝剪枝。
代码和树层剪枝只有一个true的差别。
在这个问题上,即发现和前一个数字重复,并且当前选择的位置和前一个位置相邻,则剪枝,具体见图。
其实我也不是特别理解,只能理解这个特例。 貌似是最后被剪得只剩下从后往前排列一种情况,就去掉了其他重复。
if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == true) {
continue;
}
从上图可以发现,树层剪枝不仅好用,而且好理解。
全题代码如下:
class Solution {
public:
vector<vector<int>> ans;
vector<int> path;
vector<vector<int>> permuteUnique(vector<