【力扣分模块练习】深度回溯

本文参考:carl大佬–代码随想录的题解

个人算法小抄:
1.组合问题,无序性。如{2,4}和{4,2}是同一种结果。考虑index控制。
2.排序问题,有序性。如{1,2,3}和{3,2,1}是不一样的。考虑维护used数组。
3.分割问题,虚拟出一条分割线。很多时候直接在原数据上操作及撤回。
4.子集问题,当用三部曲模板时(其实就是迭代加递归)。搜集全部经过的节点。 当使用双递归时,收集根节点即可。

77.组合 (组合问题 控制for的下界的startindex)


给定两个整数 n 和 k,返回 1 … n 中所有可能的 k 个数的组合。

样例:
输入: 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();
		}
	}
};

46. 全排列 (排列问题 维护一个used数组)


给定一个 没有重复 数字的序列,返回其所有可能的全排列。
输入: [1,2,3]
输出:
[
[1,2,3],
[1,3,2],
[2,1,3],
[2,3,1],
[3,1,2],
[3,2,1]
]

这题可以用交换来改变排列的。撤销操作就是再交换一遍。 用一个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]);
		}
	}
};

通用套路!排列问题


用一个bool的used数组来记录已访问元素。(这样就可以知道还剩哪些元素,用for遍历的时候跳过那些已经访问过的)。
注意 回溯撤销操作的时候,要把访问数组的操作也撤销

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题排列问题的剪枝问题,这个剪枝方法也很常用。

47. 全排列 II (排列剪枝 used数组+剪枝)


给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。

样例:
输入:
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;
}

【力扣分模块练习】深度回溯_第1张图片
以上就是树层剪枝。

此外还有一种树枝剪枝。
代码和树层剪枝只有一个true的差别。
在这个问题上,即发现和前一个数字重复,并且当前选择的位置和前一个位置相邻,则剪枝,具体见图。
其实我也不是特别理解,只能理解这个特例。 貌似是最后被剪得只剩下从后往前排列一种情况,就去掉了其他重复。

if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == true) {
    
    continue;
}

【力扣分模块练习】深度回溯_第2张图片

从上图可以发现,树层剪枝不仅好用,而且好理解。
全题代码如下:

class Solution {
   
public:
	vector<vector<int>> ans;
	vector<int> path;
	vector<vector<int>> permuteUnique(vector<

你可能感兴趣的:(LeetCode,算法,数据结构,leetcode)