This commit is contained in:
youngyangyang04
2020-12-24 10:14:52 +08:00
parent 80cb4d9c1e
commit f53e2b25b0
10 changed files with 335 additions and 61 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 168 KiB

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 179 KiB

After

Width:  |  Height:  |  Size: 188 KiB

View File

@ -39,8 +39,7 @@ candidates 中的数字可以无限制重复被选取。
本题搜索的过程抽象成树形结构如下:
![39.组合总和](https://img-blog.csdnimg.cn/20201123202227835.png)
![39.组合总和](https://img-blog.csdnimg.cn/20201223170730367.png)
注意图中叶子节点的返回条件因为本题没有组合数量要求仅仅是总和的限制所以递归没有层数的限制只要选取的元素总和超过target就返回
而在[回溯算法:求组合问题!](https://mp.weixin.qq.com/s/OnBjbLzuipWz_u4QfmgcqQ)和[回溯算法:求组合总和!](https://mp.weixin.qq.com/s/HX7WW6ixbFZJASkRnCTC3w) 中都可以知道要递归K层因为要取k个元素的组合。
@ -75,7 +74,7 @@ void backtracking(vector<int>& candidates, int target, int sum, int startIndex)
在如下树形结构中:
![39.组合总和](https://img-blog.csdnimg.cn/20201123202227835.png)
![39.组合总和](https://img-blog.csdnimg.cn/20201223170730367.png)
从叶子节点可以清晰看到终止只有两种情况sum大于target和sum等于target。
@ -148,7 +147,7 @@ public:
在这个树形结构中:
![39.组合总和](https://img-blog.csdnimg.cn/20201123202227835.png)
![39.组合总和](https://img-blog.csdnimg.cn/20201223170730367.png)
以及上面的版本一的代码大家可以看到对于sum已经大于target的情况其实是依然进入了下一层递归只是下一层递归结束判断的时候会判断sum > target的话就返回。
@ -160,8 +159,8 @@ public:
如图:
![39.组合总和1](https://img-blog.csdnimg.cn/20201123202349897.png)
![39.组合总和1](https://img-blog.csdnimg.cn/20201223170809182.png)
for循环剪枝代码如下

View File

@ -1,33 +1,61 @@
## 题目链接
https://leetcode-cn.com/problems/merge-intervals/
> 「代码随想录」出品,毕竟精品!
## 思路
这道题目看起来就是一道模拟类的题,但其实是一道贪心题目!
# 56. 合并区间
按照左区间排序之后,每次合并都取最大的右区间,这样就可以合并更多的区间了。
题目链接https://leetcode-cn.com/problems/merge-intervals/
那有同学问了,这不是废话么? 当然要取最大的右区间
给出一个区间的集合,请合并所有重叠的区间。
**是的,一想就是这么个道理,但它就是贪心的思想,局部最优推导出整体最优**
示例 1:
输入: intervals = [[1,3],[2,6],[8,10],[15,18]]
输出: [[1,6],[8,10],[15,18]]
解释: 区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].
这也就是为什么很多同学刷题的时候都没有发现自己用了贪心。
示例 2:
输入: intervals = [[1,4],[4,5]]
输出: [[1,5]]
解释: 区间 [1,4] 和 [4,5] 可被视为重叠区间。
注意输入类型已于2019年4月15日更改。 请重置默认代码定义以获取新方法签名。
合并思路:如果 `intervals[i][0] < intervals[i - 1][1]` 即intervals[i]起始位置 < intervals[i - 1]终止位置则一定有重复需要合并
提示:
如图所示
* intervals[i][0] <= intervals[i][1]
<img src='../pics/56.合并区间.png' width=600> </img></div>
# 思路
大家应该都感觉到了,此题一定要排序,那么按照左边界排序,还是右边界排序呢?
都可以!
那么我按照左边界排序,排序之后局部最优:每次合并都取最大的右边界,这样就可以合并更多的区间了,整体最优:合并所有重叠的区间。
局部最优可以推出全局最优,找不出反例,试试贪心。
那有同学问了,本来不就应该合并最大右边界么,这和贪心有啥关系?
有时候贪心就是常识!哈哈
按照左边界从小到大排序之后,如果 `intervals[i][0] < intervals[i - 1][1]` 即intervals[i]左边界 < intervals[i - 1]右边界则一定有重复因为intervals[i]的左边界一定是大于等于intervals[i - 1]的左边界
intervals[i]的左边界在intervals[i - 1]左边界和右边界的范围内那么一定有重复
这么说有点抽象看图**注意图中区间都是按照左边界排序之后了**
![56.合并区间](https://img-blog.csdnimg.cn/20201223200632791.png)
知道如何判断重复之后剩下的就是合并了如何去模拟合并区间呢
其实就是用合并区间后左边界和右边界作为一个新的区间加入到result数组里就可以了如果没有合并就把原区间加入到result数组
C++代码如下
```
```C++
class Solution {
public:
// 按照区间左边界排序
// 按照区间左边界从小到大排序
static bool cmp (const vector<int>& a, const vector<int>& b) {
return a[0] < b[0];
}
vector<vector<int>> merge(vector<vector<int>>& intervals) {
vector<vector<int>> result;
if (intervals.size() == 0) return result;
@ -36,13 +64,14 @@ public:
int length = intervals.size();
for (int i = 1; i < length; i++) {
int start = intervals[i - 1][0];
int end = intervals[i - 1][1];
int start = intervals[i - 1][0]; // 初始为i-1区间的左边界
int end = intervals[i - 1][1]; // 初始i-1区间的右边界
while (i < length && intervals[i][0] <= end) { // 合并区间
end = max(end, intervals[i][1]);
end = max(end, intervals[i][1]); // 不断更新右区间
if (i == length - 1) flag = true; // 最后一个区间也合并了
i++;
i++; // 继续合并下一个区间
}
// start和end是表示intervals[i - 1]的左边界右边界所以最优intervals[i]区间是否合并了要标记一下
result.push_back({start, end});
}
// 如果最后一个区间没有合并将其加入result
@ -53,3 +82,47 @@ public:
}
};
```
当然以上代码有冗余一些,可以优化一下,如下:(思路是一样的)
```C++
class Solution {
public:
vector<vector<int>> merge(vector<vector<int>>& intervals) {
vector<vector<int>> result;
if (intervals.size() == 0) return result;
// 排序的参数使用了lamda表达式
sort(intervals.begin(), intervals.end(), [](const vector<int>& a, const vector<int>& b){return a[0] < b[0];});
result.push_back(intervals[0]);
for (int i = 1; i < intervals.size(); i++) {
if (result.back()[1] >= intervals[i][0]) { // 合并区间
result.back()[1] = max(result.back()[1], intervals[i][1]);
} else {
result.push_back(intervals[i]);
}
}
return result;
}
};
```
* 时间复杂度O(nlogn) 有一个快排
* 空间复杂度O(1)我没有算result数组返回值所需容器占的空间
# 总结
对于贪心算法很多同学都是**如果能凭常识直接做出来就会感觉不到自己用了贪心, 一旦第一直觉想不出来, 可能就一直想不出来了**。
跟着代码随想录刷题的录友应该感受过贪心难起来真的难
那应该怎么办呢
正如我贪心系列开篇词[关于贪心算法,你该了解这些!](https://mp.weixin.qq.com/s/O935TaoHE9Eexwe_vSbRAg)中讲解的一样贪心本来就没有套路也没有框架所以各种常规解法需要多接触多练习自然而然才会想到
代码随想录会把贪心常见的经典题目覆盖到大家只要认真学习打卡就可以了
就酱学算法就在代码随想录」,值得介绍给身边的朋友同学们

View File

@ -1,4 +1,43 @@
# 思路
本题大家多举一个例子,就发现这其实就是斐波那契数列。
题目509. 斐波那契数中的代码初始化部分稍加改动,就可以过了本题。
C++代码如下:
```
class Solution {
public:
int climbStairs(int n) {
if (n <= 1) return n;
vector<int> dp(n + 1);
dp[0] = 1;
dp[1] = 1;
for (int i = 2; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
};
```
既然这么简单为什么还要讲呢,其实本题稍加改动就是一道面试好题,如果每次可以爬 1 或 2或3或者m 个台阶呢,走到楼顶有几种方法?
* 确定dp数组以及下标的含义
dp[i]爬到有i个台阶的楼顶有dp[i]种方法
* 确定递推公式
dp[i]有几种来源dp[i - 1]dp[i - 2]
* dp数组如何初始化
* 确定遍历顺序
dp里求排列1 2 步 和 2 1 步都是上三个台阶,但不一样!
这是求排列
@ -17,3 +56,19 @@ public:
}
};
```
# 总结
如果我来面试的话,我就会想给候选人出一个 本题原题,看其表现,如果顺利写出来,进而在要求每次可以爬[1 - m]个台阶应该怎么写。
顺便再考察一下两个for循环的嵌套顺序为什么target放外面nums放里面。这就能反馈出对背包问题本质的掌握程度是不是刷题背公式一眼就看出来。
这么一连套下来,如果候选人都能答出来,相信任何一位面试官都是非常满意的。
**本题代码不长题目也很普通当稍稍一进阶就可以考察本质问题而且题目进阶的内容在leetcode上并没有一定程度上就可以排除掉刷题党了简直是面试题目的绝佳选择**
相信通过这道简单的斐波那契数列题目,大家能感受到大厂面试官最喜欢什么样的面试题目了,并不是手撕红黑树!
所以本题是一道非常好的题目。

View File

@ -1,5 +1,103 @@
[1] 0 输出的是0不是-1啊这颗真是天坑j
# 思路
* 确定dp数组以及下标的含义
dp[j]凑足总额为j所需钱币的最少个数为dp[j]
* 确定递推公式
得到dp[j]有考虑coins[i]有两个来源一个是dp[j - coins[i]]一个是dp[j] 没有考虑coins[i])自己。
凑足总额为j - coins[i]的最少个数为dp[j - coins[i]]那么只需要加上一个钱币coins[i]即dp[j - coins[i]] + 1就是dp[j]考虑coins[i]
所以dp[j]有考虑coins[i]有两个选择dp[j]没有考虑coins[i]和dp[j - coins[i]] + 1考虑coins[i]),一定是取最小的。
dp[j] = min(dp[j - coins[i]] + 1, dp[j]);
* dp数组如何初始化
首先凑足总金额为0所需钱币的个数一定是0那么dp[0] = 0;
其他下标对应的数值呢?
考虑到递推公式的特性dp[j]必须初始化为一个最大的数否则就会在min(dp[j - coins[i]] + 1, dp[j])比较的过程中比初始值覆盖。
所以下标非0的元素都是应该是最大值。
代码如下:
```
vector<int> dp(amount + 1, INT_MAX);
dp[0] = 0;
```
* 确定遍历顺序
求钱币最小个数,那么钱币有顺序,和钱币没有顺序都可以,都不影响钱币的最小个数。可以用背包组合方式或者排列方式来求。
如果本题要是求组成amount的有几种方式那么钱币循序就有影响了。
所以两个for循环的关系是coins放在外循环target在内循环、或者target放在外循环coins在内循环都是可以的
那么我采用coins放在外循环target在内循环的方式。
本题钱币数量可以无限使用,那么是完全背包。所以遍历的内循环是正序
综上所述遍历顺序为coins放在外循环target在内循环。且内循环正序。
C++ 代码如下:
```
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
vector<int> dp(amount + 1, INT_MAX);
dp[0] = 0;
for (int i = 0 ;i < coins.size(); i++) { // 遍历钱币
for (int j = coins[i]; j <= amount; j++) { // 遍历target
if (dp[j - coins[i]] != INT_MAX) { // 如果dp[j - coins[i]]是初始值则跳过
dp[j] = min(dp[j - coins[i]] + 1, dp[j]);
}
}
}
if (dp[amount] == INT_MAX) return -1;
return dp[amount];
}
};
```
# 拓展
对于遍历方式target放在外循环coins在内循环都是可以的只不过对应的初始化操作有点微调我就直接给出代码了
```C++
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
vector<int> dp(amount + 1, INT_MAX);
dp[0] = 0;
for (int i = 1; i <= amount; i++) {
for (int j = 0; j < coins.size(); j++) {
if (i - coins[j] >= 0 && dp[i - coins[j]] != INT_MAX ) {
dp[i] = min(dp[i - coins[j]] + 1, dp[i]);
}
}
}
if (dp[amount] == INT_MAX) return -1;
return dp[amount];
}
};
```
# 总结
相信大家看网上的题解一篇是遍历amount的for循环放外面一篇是遍历amount的for循环放里面看多了都看晕了能把 遍历顺序讲明白的文章非常少。
这也是大多数同学学习动态规划的苦恼所在,有的时候递推公式其实很简单,但遍历顺序很难把握!
那么Carl就把遍历顺序分析的清清楚楚相信大家看完之后对背包问题又了更深的理解了。
# tmp
```
// dp初始化很重要
@ -33,40 +131,17 @@ public:
};
```
我用求组合的思路也过了,
```
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
//int dp[10003] = {0}; // 并没有给所有元素赋值0
//if (amount == 0) return 0;
vector<int> dp(10003, INT_MAX);
dp[0] = 0;
for (int i = 0 ;i < coins.size(); i++) { // 求组合
for (int j = 1; j <= amount; j++) {
if (j - coins[i] >= 0 && dp[j - coins[i]] != INT_MAX) {
dp[j] = min(dp[j - coins[i]] + 1, dp[j]);
}
}
}
if (dp[amount] == INT_MAX) return -1;
return dp[amount];
}
};
```
这种标记d代码简短但思路有点绕
```
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
//int dp[10003] = {0}; // 并没有给所有元素赋值0
// if (amount == 0) return 0; 这个都可以省略了,但很多同学不知道 还需要注意这个
vector<int> dp(10003, 0);
if (amount == 0) return 0; // 这个要注意
vector<int> dp(amount + 1, 0);
for (int i = 1; i <= amount; i++) {
dp[i] = INT_MAX;
for (int j = 0; j < coins.size(); j++) {
if (i - coins[j] >= 0 && dp[i - coins[j]]!=INT_MAX ) {
if (i - coins[j] >= 0 && dp[i - coins[j]] != INT_MAX) {
dp[i] = min(dp[i - coins[j]] + 1, dp[i]);
}
}
@ -76,3 +151,5 @@ public:
}
};
```

View File

@ -162,6 +162,8 @@ public:
大家可以把两个版本的代码提交一下试试就可以发现其差别了
关于本题使用数组还是使用链表的性能差异我在[贪心算法:根据身高重建队列(续集)](https://mp.weixin.qq.com/s/K-pRN0lzR-iZhoi-1FgbSQ)中详细讲解了一波
# 总结
关于出现两个维度一起考虑的情况我们已经做过两道题目了另一道就是[贪心算法:分发糖果](https://mp.weixin.qq.com/s/8MwlgFfvaNYmjGwjuMlETQ)

View File

@ -94,7 +94,7 @@ if ((S + sum) % 2 == 1) return 0; // 此时没有方案两个int相加的时
这次和之前遇到的背包问题不一样了之前都是求容量为j的背包最多能装多少。
本题是装满有几种方法。
本题是装满有几种方法。其实这就是一个组合问题了。
* 确定dp数组以及下标的含义
@ -102,13 +102,50 @@ dp[j] 表示填满j包括j这么大容积的包有dp[i]种方法
* 确定递推公式
有哪些来源可以推出dp[j]呢只有dp[j - nums[i]]。
有哪些来源可以推出dp[j]呢
那么dp[j] 应该是 dp[j] + dp[j - nums[i]] **这块需要好好讲讲**
不考虑nums[i]的情况下填满容量为j - nums[i]的背包有dp[j - nums[i]]中方法。
那么如果考虑nums[i]呢dp[j] = dp[j] + dp[j - nums[i]];
公式右面的dp[j]填满容量为j的背包没有考虑nums[i]有dp[j]种方法,
公式右面的dp[j - nums[i]]填满容量为j - nums[i]的背包有dp[j - nums[i]]种方法
那么只要搞到nums[i]的话就应该dp[j]考虑nums[i]= dp[j]没考虑nums[i] + dp[j - nums[i]]
举一个例子,nums[i] = 2 dp[5] = dp[5] + dp[3]公式右边的dp[5]没考虑这个2就有dp[5]种方法。
填满背包容量为3的话有dp[3]种方法。
那么只需要搞到一个2nums[i]有dp[3]方法可以凑齐容量为3的背包相应的就有多少种方法可以凑齐容量为5的背包。
所以 dp[5]考虑2 = dp[5]没考虑2 + dp[3]。
所以求组合类问题的公式,都是类似这种:
```
dp[j] += dp[j - num[i]]
```
**这个公式在后面在讲解背包解决排列组合问题的时候还会用到!**
* dp数组如何初始化
从递归公式可以看出在初始化的时候dp[0] 一定要初始化为1因为dp[0]是在公式中一切递推结果的起源如果dp[0]是0的话递归结果将都是0。
dp[0] = 1理论上也很好解释装满容量为0的背包有1中方法就是装0件物品。
dp[j]其他下标对应的数值应该初始化为0从递归公式也可以看出dp[j]要保证是0的初始值才能正确的由dp[j - nums[i]]推导出来。
* 确定遍历顺序
对于01背包问题一维dp的遍历nums放在外循环target在内循环且内循环倒序。
C++代码如下:
```
class Solution {
public:
@ -119,7 +156,7 @@ public:
if ((S + sum) % 2 == 1) return 0; // 此时没有方案两个int相加的时候要各位小心数值溢出的问题
int bagSize = (S + sum) / 2;
int dp[1001] = {1};
int dp[1001] = {1}; // 注意这个语法是第一个元素为1其他都是0
for (int i = 0; i < nums.size(); i++) {
for (int j = bagSize; j >= nums[i]; j--) {
dp[j] += dp[j - nums[i]];

View File

@ -10,16 +10,17 @@
动态规划中dp[j]是又dp[j-weight[i]]推导出来的。
但如果是贪心呢,dp[j]每次选一个最大的或者最小的就完事了。
但如果是贪心呢,每次拿物品选一个最大的或者最小的就完事了。
所以贪心解决不了动态规划的问题,这也是最大的区别。
很多讲解动态规划的文章都会讲最优子结构啊和重叠子问题啊这些,这些东西都是教科书的上定义,晦涩难懂而且不实用
大家也不用死扣动规和贪心的理论区别,后面做做题目自然就知道了
大家只要知道,动规是由前一个状态推导出来的,而贪心是局部直接算最优的,就够用了
而且很多讲解动态规划的文章都会讲最优子结构啊和重叠子问题啊这些,这些东西都是教科书的上定义,晦涩难懂而且不实用
对于上述提到的背包问题,后序会详细讲解
大家知道动规是由前一个状态推导出来的,而贪心是局部直接选最优的,对于刷题来说就够用了
上述提到的背包问题,后序会详细讲解。
# 动态规划的解题步骤
@ -80,7 +81,18 @@
然后在问问题,目的性就很强了,回答问题的同学也可以快速知道提问者的疑惑了。
# 动态规划可以解决哪一类问题
# 总结
这一篇是动态规划的整体概述讲解了什么是动态规划动态规划的解题步骤以及如何debug。
动态规划是一个很大的领域,今天这一篇讲解的内容是整个动态规划系列里都会使用到的一些理论基础。
在后序讲解中针对某一具体问题还会讲解其对应的理论基础例如背包问题中的01背包leetcode上的题目都是01背包的应用而没有纯01背包的问题那么就需要在把对应的理论知识讲解一下。
一些同学可能着急想刷题,这个我很理解,我写的理论基础篇已经是非常偏实用的了,还是需要一点基础的。新加入的录友可能不了解,可以在「算法汇总」中看到每一个系列开始的时候都有对应的理论基础篇,都是特别实用的理论基础了。
今天我们开始新的征程了,你准备好了么?

View File

@ -0,0 +1,19 @@
组合问题公式:dp[i] += dp[i-num]
True、False问题公式:dp[i] = dp[i] or dp[i-num]
最大最小问题公式:dp[i] = min(dp[i], dp[i-num]+1)或者dp[i] = max(dp[i], dp[i-num]+1)
接下来讲一下背包问题的判定
背包问题具备的特征给定一个targettarget可以是数字也可以是字符串再给定一个数组numsnums中装的可能是数字也可能是字符串能否使用nums中的元素做各种排列组合得到target。
背包问题技巧:
1.如果是0-1背包即数组中的元素不可重复使用nums放在外循环target在内循环且内循环倒序
for num in nums:
for i in range(target, nums-1, -1):
2.如果是完全背包即数组中的元素可重复使用nums放在外循环target在内循环。且内循环正序。
for num in nums:
for i in range(nums, target+1):
3.如果组合问题需考虑元素之间的顺序需将target放在外循环将nums放在内循环。
for i in range(1, target+1):
for num in nums: