diff --git a/README.md b/README.md index 7bd626f5..d1430d96 100644 --- a/README.md +++ b/README.md @@ -135,6 +135,8 @@ * [回溯算法:组合问题再剪剪枝](https://mp.weixin.qq.com/s/Ri7spcJMUmph4c6XjPWXQA) * [回溯算法:求组合总和!](https://mp.weixin.qq.com/s/HX7WW6ixbFZJASkRnCTC3w) * [回溯算法:电话号码的字母组合](https://mp.weixin.qq.com/s/e2ua2cmkE_vpYjM3j6HY0A) + * [本周小结!(回溯算法系列一)](https://mp.weixin.qq.com/s/m2GnTJdkYhAamustbb6lmw) + * [回溯算法:求组合总和(二)](https://mp.weixin.qq.com/s/FLg8G6EjVcxBjwCbzpACPw) (持续更新中....) @@ -303,6 +305,7 @@ |[0039.组合总和](https://github.com/youngyangyang04/leetcode/blob/master/problems/0039.组合总和.md) |数组/回溯 |中等| **回溯**| |[0040.组合总和II](https://github.com/youngyangyang04/leetcode/blob/master/problems/0040.组合总和II.md) |数组/回溯 |中等| **回溯**| |[0042.接雨水](https://github.com/youngyangyang04/leetcode/blob/master/problems/0042.接雨水.md) |数组/栈/双指针 |困难| **双指针** **单调栈** **动态规划**| +|[0045.跳跃游戏II](https://github.com/youngyangyang04/leetcode/blob/master/problems/0045.跳跃游戏II.md) |贪心 |困难| **贪心**| |[0046.全排列](https://github.com/youngyangyang04/leetcode/blob/master/problems/0046.全排列.md) |回溯|中等| **回溯**| |[0047.全排列II](https://github.com/youngyangyang04/leetcode/blob/master/problems/0047.全排列II.md) |回溯|中等| **回溯**| |[0051.N皇后](https://github.com/youngyangyang04/leetcode/blob/master/problems/0051.N皇后.md) |回溯|困难| **回溯**| diff --git a/pics/39.组合总和1.png b/pics/39.组合总和1.png index f4a2464b..ae89ab03 100644 Binary files a/pics/39.组合总和1.png and b/pics/39.组合总和1.png differ diff --git a/pics/40.组合总和II.png b/pics/40.组合总和II.png index 59cc660b..104e5f4e 100644 Binary files a/pics/40.组合总和II.png and b/pics/40.组合总和II.png differ diff --git a/pics/40.组合总和II1.png b/pics/40.组合总和II1.png new file mode 100644 index 00000000..75fda6b3 Binary files /dev/null and b/pics/40.组合总和II1.png differ diff --git a/pics/45.跳跃游戏II.png b/pics/45.跳跃游戏II.png new file mode 100644 index 00000000..bcf956e5 Binary files /dev/null and b/pics/45.跳跃游戏II.png differ diff --git a/problems/0039.组合总和.md b/problems/0039.组合总和.md index 6d3efa8c..7f624938 100644 --- a/problems/0039.组合总和.md +++ b/problems/0039.组合总和.md @@ -177,9 +177,6 @@ private: vector> result; vector path; void backtracking(vector& candidates, int target, int sum, int startIndex) { - if (sum > target) { - return; - } if (sum == target) { result.push_back(path); return; diff --git a/problems/0040.组合总和II.md b/problems/0040.组合总和II.md index f1394d2b..6ff491df 100644 --- a/problems/0040.组合总和II.md +++ b/problems/0040.组合总和II.md @@ -1,56 +1,151 @@ -# 第40题. 组合总和 +> 这篇可以说是全网把组合问题如何去重,讲的最清晰的了! + + +# 40.组合总和II + +题目链接:https://leetcode-cn.com/problems/combination-sum-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] +] -示例 1: +# 思路 -输入: candidates = [10,1,2,7,6,1,5], target = 8, -所求解集为: -[ - [1, 7], - [1, 2, 5], - [2, 6], - [1, 1, 6] -] +这道题目和[39.组合总和](https://mp.weixin.qq.com/s/FLg8G6EjVcxBjwCbzpACPw)如下区别: -示例 2: +1. 本题candidates 中的每个数字在每个组合中只能使用一次。 +2. 本题数组candidates的元素是有重复的,而[39.组合总和](https://mp.weixin.qq.com/s/FLg8G6EjVcxBjwCbzpACPw)是无重复元素的数组candidates -输入: candidates = [2,5,2,1,2], target = 5, -所求解集为: -[ -  [1,2,2], -  [5] -] +最后本题和[39.组合总和](https://mp.weixin.qq.com/s/FLg8G6EjVcxBjwCbzpACPw)要求一样,解集不能包含重复的组合。 -# 思想 +**本题的难点在于区别2中:集合(数组candidates)有重复元素,但还不能有重复的组合**。 -这道题目和[0039.组合总和](https://github.com/youngyangyang04/leetcode/blob/master/problems/0039.组合总和.md) 区别就是要去重。 +一些同学可能想了:我把所有组合求出来,再用set或者map去重,这么做很容易超时! -很多同学在去重上想不明白,其实很多题解也没有讲清楚,反正代码是能过的,感觉是那么回事,稀里糊涂的先把题目过了。 +所以要在搜索的过程中就去掉重复组合。 + +很多同学在去重的问题上想不明白,其实很多题解也没有讲清楚,反正代码是能过的,感觉是那么回事,稀里糊涂的先把题目过了。 这个去重为什么很难理解呢,**所谓去重,其实就是使用过的元素不能重复选取。** 这么一说好像很简单! - 都知道组合问题可以抽象为树形结构,那么“使用过”在这个树形结构上是有两个维度的,一个维度是同一树枝上使用过,一个维度是同一树层上使用过。**没有理解这两个层面上的“使用过” 是造成大家没有彻底理解去重的根本原因。** -所以要明确我们要去重的是同一树层上的“使用过”。 +那么问题来了,我们是要同一树层上使用过,还是统一树枝上使用过呢? + +回看一下题目,元素在同一个组合内是可以重复的,怎么重复都没事,但两个组合不能相同。 + +**所以我们要去重的是同一树层上的“使用过”,同一树枝上的都是一个组合里的元素,不用去重**。 为了理解去重我们来举一个例子,candidates = [1, 1, 2], target = 3,(方便起见candidates已经排序了) -选择过程如图所示: +选择过程树形结构如图所示: +可以看到图中,每个节点相对于 [39.组合总和](https://mp.weixin.qq.com/s/FLg8G6EjVcxBjwCbzpACPw)我多加了used数组,这个used数组下面会重点介绍。 -理解了“同一树枝使用过”和“同一树层使用过” 之后,我们在拉看如下代码实现,关键地方已经注释,大家应该就理解了 +## 回溯三部曲 -# C++代码 +* **递归函数参数** + +与[39.组合总和](https://mp.weixin.qq.com/s/FLg8G6EjVcxBjwCbzpACPw)套路相同,此题还需要加一个bool型数组used,用来记录同一树枝上的元素是否使用过。 + +这个集合去重的重任就是used来完成的。 + +代码如下: + +``` +vector> result; // 存放组合集合 +vector path; // 符合条件的组合 +void backtracking(vector& candidates, int target, int sum, int startIndex, vector& used) { +``` + +* **递归终止条件** + +与[39.组合总和](https://mp.weixin.qq.com/s/FLg8G6EjVcxBjwCbzpACPw)相同,终止条件为 `sum > target` 和 `sum == target`。 + +代码如下: + +``` +if (sum > target) { // 这个条件其实可以省略 + return; +} +if (sum == target) { + result.push_back(path); + return; +} +``` + +`sum > target` 这个条件其实可以省略,因为和在递归单层遍历的时候,会有剪枝的操作,下面会介绍到。 + +* **单层搜索的逻辑** + +这里与[39.组合总和](https://mp.weixin.qq.com/s/FLg8G6EjVcxBjwCbzpACPw)最大的不同就是要去重了。 + +前面我们提到:要去重的是“同一树层上的使用过”,如果判断同一树层上元素(相同的元素)是否使用过了呢。 + +**如果`candidates[i] == candidates[i - 1]` 并且 `used[i - 1] == false`,就说明:前一个树枝,使用了candidates[i - 1],也就是说同一树层使用过candidates[i - 1]**。 + +此时for循环里就应该做continue的操作。 + +这块比较抽象,如图: + + + +我在图中将used的变化用橘黄色标注上,可以看出在candidates[i] == candidates[i - 1]相同的情况下: + +* used[i - 1] == true,说明同一树支candidates[i - 1]使用过 +* used[i - 1] == false,说明同一树层candidates[i - 1]使用过 + +**这块去重的逻辑很抽象,网上搜的题解基本没有能讲清楚的,如果大家之前思考过这个问题或者刷过这道题目,看到这里一定会感觉通透了很多!** + +那么单层搜索的逻辑代码如下: + +``` +for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++) { + // used[i - 1] == true,说明同一树支candidates[i - 1]使用过 + // used[i - 1] == false,说明同一树层candidates[i - 1]使用过 + // 要对同一树层使用过的元素进行跳过 + if (i > 0 && candidates[i] == candidates[i - 1] && used[i - 1] == false) { + continue; + } + sum += candidates[i]; + path.push_back(candidates[i]); + used[i] = true; + backtracking(candidates, target, sum, i + 1, used); // 和39.组合总和的区别1:这里是i+1,每个数字在每个组合中只能使用一次 + used[i] = false; + sum -= candidates[i]; + path.pop_back(); +} +``` + +**注意sum + candidates[i] <= target为剪枝操作,在[39.组合总和](https://mp.weixin.qq.com/s/FLg8G6EjVcxBjwCbzpACPw)有讲解过!** + +## C++代码 + +回溯三部曲分析完了,整体C++代码如下: ``` class Solution { @@ -58,28 +153,21 @@ private: vector> result; vector path; void backtracking(vector& candidates, int target, int sum, int startIndex, vector& used) { - if (sum > target) { - return; - } if (sum == target) { result.push_back(path); return; } - - // 每个组合中只能使用一次 所以用 startindex - // 给定一个数组 candidates 默认有重复项,解集不能包含重复的组合。 所以使用if这一套 - for (int i = startIndex; i < candidates.size(); i++) { - // 这里理解used[i - 1]非常重要 - // used[i - 1] == true,说明同一树支candidates[i - 1]使用过 + for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++) { + // used[i - 1] == true,说明同一树支candidates[i - 1]使用过 // used[i - 1] == false,说明同一树层candidates[i - 1]使用过 - // 而我们要对同一树层使用过的元素进行跳过 - if (i > 0 && candidates[i] == candidates[i - 1] && used[i - 1] == false) { + // 要对同一树层使用过的元素进行跳过 + if (i > 0 && candidates[i] == candidates[i - 1] && used[i - 1] == false) { continue; } sum += candidates[i]; path.push_back(candidates[i]); used[i] = true; - backtracking(candidates, target, sum, i + 1, used); // 关键点在这里,不用i+1了 + backtracking(candidates, target, sum, i + 1, used); // 和39.组合总和的区别1,这里是i+1,每个数字在每个组合中只能使用一次 used[i] = false; sum -= candidates[i]; path.pop_back(); @@ -89,11 +177,25 @@ private: public: vector> combinationSum2(vector& candidates, int target) { vector used(candidates.size(), false); - // 首先把给candidates排序,让其相同的元素都挨在一起。 + path.clear(); + result.clear(); + // 首先把给candidates排序,让其相同的元素都挨在一起。 sort(candidates.begin(), candidates.end()); backtracking(candidates, target, 0, 0, used); return result; - } }; + ``` + +# 总结 + +本题同样是求组合总和,但就是因为其数组candidates有重复元素,而要求不能有重复的组合,所以相对于[39.组合总和](https://mp.weixin.qq.com/s/FLg8G6EjVcxBjwCbzpACPw)难度提升了不少。 + +**关键是去重的逻辑,代码很简单,网上一搜一大把,但几乎没有能把这块代码含义讲明白的,基本都是给出代码,然后说这就是去重了,究竟怎么个去重法也是模棱两可**。 + +所以Carl有必要把去重的这块彻彻底底的给大家讲清楚,**就连“树层去重”和“树枝去重”都是我自创的词汇,希望对大家理解有帮助!** + +**就酱,如果感觉「代码随想录」诚意满满,就帮Carl宣传一波吧,感谢啦!** + + diff --git a/problems/0045.跳跃游戏II.md b/problems/0045.跳跃游戏II.md new file mode 100644 index 00000000..a079b666 --- /dev/null +++ b/problems/0045.跳跃游戏II.md @@ -0,0 +1,76 @@ +## 题目链接 +https://leetcode-cn.com/problems/jump-game-ii/ + +## 思路 + +本题相对于[0055.跳跃游戏](https://github.com/youngyangyang04/leetcode/blob/master/problems/0053.最大子序和.md)还是难了不少。 + +本题要计算最大步数,那么就要想清楚什么时候步数加一? + +**这里需要统计两个距离,当前可移动距离和下一步最远距离**。 + +如果移动范围超过当前可移动距离,那么就必须再走一步来达到增加可移动距离的目的。 + +如图: + + + +### 方法一 + +这里还是有个特殊情况需要考虑,如果当前可移动距离的终点就是是集合终点,那么就不用增加步数了,因为不能再往后走了。 + +详情可看代码(详细注释) + +``` +// 版本一 +class Solution { +public: + int jump(vector& nums) { + if (nums.size() == 1) return 0; + int curDistance = 0; // 当前可移动距离 + int ans = 0; // 记录走的最大步数 + int nextDistance = 0; // 下一步最远距离 + for (int i = 0; i < nums.size(); i++) { + nextDistance = max(nums[i] + i, nextDistance); + if (i == curDistance) { // 遇到当前可移动距离的终点 + if (curDistance != nums.size() - 1) { // 如果当前可移动距离的终点不是集合终点 + ans++; // 需要走下一步 + curDistance = nextDistance; // 更新下一步最远距离的范围 + if (nextDistance >= nums.size() - 1) break; // 下一步最远距离已经可以达到终点,结束循环 + } else break; // 当前可移动距离的终点是集合终点 + } + } + return ans; + } +}; +``` + +### 方法二 + +依然是贪心,思路和方法一差不多,代码可以简洁一些。 + +在方法一种,处理 当前可移动距离的终点 是不是集合终点 来判断ans是否要做相应的加一操作。 + +其实可以用 for循环遍历的时候i < nums.size() - 1,这样就是默认最后一步,一定是可以到终点的。 + +代码如下: + +``` + +class Solution { +public: + int jump(vector& nums) { + int curDistance = 0; + int ans = 0; // 记录走的最大步数,初始为0 + int nextDistance = 0; // 每走一步获得的跳跃范围 + for (int i = 0; i < nums.size() - 1; i++) { // 注意这里是小于nums.size() - 1 + nextDistance = max(nums[i] + i, nextDistance); + if (i == curDistance) { // 遇到本次跳跃范围的终点 + curDistance = nextDistance; + ans++; + } + } + return ans; + } +}; +``` diff --git a/problems/0349.两个数组的交集.md b/problems/0349.两个数组的交集.md index 408ef931..2b4fef56 100644 --- a/problems/0349.两个数组的交集.md +++ b/problems/0349.两个数组的交集.md @@ -22,9 +22,9 @@ https://leetcode-cn.com/problems/intersection-of-two-arrays/ 这道题用暴力的解法时间复杂度是O(n^2),那来看看使用哈希法进一步优化。 -可以发现,貌似用数组做哈希表可以解决这道题目,把nums1的元素,映射到哈希数组的下表上,然后在遍历nums2的时候,判断是否出现过就可以了。 +那么用数组来做哈希表也是不错的选择,例如[242. 有效的字母异位词](https://mp.weixin.qq.com/s/vM6OszkM6L1Mx2Ralm9Dig),[0383.赎金信](https://mp.weixin.qq.com/s/sYZIR4dFBrw_lr3eJJnteQ) -但是要注意,**使用数据来做哈希的题目,都限制了数值的大小,例如[哈希表:可以拿数组当哈希表来用,但哈希值不要太大](https://mp.weixin.qq.com/s/vM6OszkM6L1Mx2Ralm9Dig)题目中只有小写字母,或者数值大小在[0- 10000] 之内等等。** +但是要注意,**使用数据来做哈希的题目,都限制了数值的大小。** 而这道题目没有限制数值的大小,就无法使用数组来做哈希表了。