diff --git a/README.md b/README.md index a57364f7..e2da4680 100644 --- a/README.md +++ b/README.md @@ -59,11 +59,11 @@ * 求职 * [程序员的简历应该这么写!!(附简历模板)](https://mp.weixin.qq.com/s/nCTUzuRTBo1_R_xagVszsA) * [BAT级别技术面试流程和注意事项都在这里了](https://mp.weixin.qq.com/s/815qCyFGVIxwut9I_7PNFw) - * [深圳原来有这么多互联网公司,你都知道么?](https://mp.weixin.qq.com/s/3VJHF2zNohBwDBxARFIn-Q) - * [北京有这些互联网公司,你都知道么?]() + * [北京有这些互联网公司,你都知道么?](https://mp.weixin.qq.com/s/BKrjK4myNB-FYbMqW9f3yw) * [上海有这些互联网公司,你都知道么?]() - * [成都有这些互联网公司,你都知道么?](https://mp.weixin.qq.com/s/Y9Qg22WEsBngs8B-K8acqQ) + * [深圳原来有这么多互联网公司,你都知道么?](https://mp.weixin.qq.com/s/3VJHF2zNohBwDBxARFIn-Q) * [广州有这些互联网公司,你都知道么?](https://mp.weixin.qq.com/s/Ir_hQP0clbnvHrWzDL-qXg) + * [成都有这些互联网公司,你都知道么?](https://mp.weixin.qq.com/s/Y9Qg22WEsBngs8B-K8acqQ) * 算法性能分析 @@ -217,6 +217,7 @@ * [贪心算法:根据身高重建队列](https://mp.weixin.qq.com/s/-2TgZVdOwS-DvtbjjDEbfw) * [本周小结!(贪心算法系列三)](https://mp.weixin.qq.com/s/JfeuK6KgmifscXdpEyIm-g) * [贪心算法:根据身高重建队列(续集)](https://mp.weixin.qq.com/s/K-pRN0lzR-iZhoi-1FgbSQ) + * [贪心算法:用最少数量的箭引爆气球](https://mp.weixin.qq.com/s/HxVAJ6INMfNKiGwI88-RFw) * 动态规划 diff --git a/problems/0406.根据身高重建队列.md b/problems/0406.根据身高重建队列.md index 14441ecc..80cba6f9 100644 --- a/problems/0406.根据身高重建队列.md +++ b/problems/0406.根据身高重建队列.md @@ -130,8 +130,6 @@ public: 所以使用vector(动态数组)来insert,是费时的,插入再拷贝的话,单纯一个插入的操作就是O(n^2)了,甚至可能拷贝好几次,就不止O(n^2)了。 -对于本题,不用vector的话,如果有多少人就申请多大的固定数组来模拟插入操作也可以,这样就不会有扩充数组的情况,但是就要真的手动模拟插入的操作了,比较麻烦。 - 改成链表之后,C++代码如下: ```C++ diff --git a/problems/0416.分割等和子集.md b/problems/0416.分割等和子集.md index 9cf5d0c2..b5eb3f75 100644 --- a/problems/0416.分割等和子集.md +++ b/problems/0416.分割等和子集.md @@ -44,6 +44,11 @@ * 背包如何正好装满,说明找到了总和为 sum / 2 的子集。 * 背包中每一个元素一定是不可重复放入。 +**按照这四部在分析一波** +* 确定dp数组以及下标的含义 +* 确定递推公式 +* dp数组如何初始化 +* 确定遍历顺序 定义里数组为dp[],dp[i] 表示 背包中放入体积为i的商品,最大价值为dp[i]。 diff --git a/problems/0435.无重叠区间.md b/problems/0435.无重叠区间.md index fcb2a9af..f4b77e88 100644 --- a/problems/0435.无重叠区间.md +++ b/problems/0435.无重叠区间.md @@ -1,22 +1,70 @@ +

+ +

+

+ + + + + + +

+ + +> 代码很简单,思路很高端 + +# 435. 无重叠区间 + +题目链接:https://leetcode-cn.com/problems/non-overlapping-intervals/ + +给定一个区间的集合,找到需要移除区间的最小数量,使剩余区间互不重叠。 + +注意: +可以认为区间的终点总是大于它的起点。 +区间 [1,2] 和 [2,3] 的边界相互“接触”,但没有相互重叠。 + +示例 1: +输入: [ [1,2], [2,3], [3,4], [1,3] ] +输出: 1 +解释: 移除 [1,3] 后,剩下的区间没有重叠。 + +示例 2: +输入: [ [1,2], [1,2], [1,2] ] +输出: 2 +解释: 你需要移除两个 [1,2] 来使剩下的区间没有重叠。 + +示例 3: +输入: [ [1,2], [2,3] ] +输出: 0 +解释: 你不需要移除任何区间,因为它们已经是无重叠的了。 ## 思路 -这道题目如果真的去模拟去重复区间的行为,是非常麻烦的,还要有删除区间。 - **相信很多同学看到这道题目都冥冥之中感觉要排序,但是究竟是按照右边界排序,还是按照左边界排序呢?** -按照右边界排序,从左向右遍历,右边界越小越好,因为右边界越小,留给下一个区间的空间就越大,所以可以从左向右遍历,优先选右边界小的。 +这其实是一个难点! -按照左边界排序,那么就是从右向左遍历,左边界数值越大越好(越靠右),这样就给前一个区间的空间就越大,所以可以从右向左遍历。 +按照右边界排序,就要从左向右遍历,因为右边界越小越好,只要右边界越小,留给下一个区间的空间就越大,所以从左向右遍历,优先选右边界小的。 -如果按照左边界排序,还从左向右遍历的话,要处理各个区间右边界的各种情况,就比较复杂了,这其实也就不是贪心了。 +按照左边界排序,就要从右向左遍历,因为左边界数值越大越好(越靠右),这样就给前一个区间的空间就越大,所以可以从右向左遍历。 +如果按照左边界排序,还从左向右遍历的话,要处理各个区间右边界的各种情况,就很复杂了。 + +一些同学做这道题目可能真的去模拟去重复区间的行为,这是比较麻烦的,还要去删除区间。 + +题目只是要求移除区间的个数,没有必要去真实的模拟删除区间! **我来按照右边界排序,从左向右记录非交叉区间的个数。最后用区间总数减去非交叉区间的个数就是需要移除的区间个数了**。 +此时问题就是要求非交叉区间的最大个数。 + +右边界排序之后,局部最优:优先选右边界小的区间,所以从左向右遍历,留给下一个区间的空间大一些,从而尽量避免交叉。全局最优:选取最多的非交叉区间。 + +局部最优推出全局最优,试试贪心! + 这里记录非交叉区间的个数还是有技巧的,如图: - +![435.无重叠区间](https://img-blog.csdnimg.cn/20201221201553618.png) 区间,1,2,3,4,5,6都按照右边界排好序。 @@ -26,7 +74,7 @@ 区间4结束之后,在找到区间6,所以一共记录非交叉区间的个数是三个。 -总共区间个数为6,减去非交叉区间的个数(3),为3。移除区间的最小数量就是3。 +总共区间个数为6,减去非交叉区间的个数3。移除区间的最小数量就是3。 C++代码如下: @@ -41,7 +89,7 @@ public: if (intervals.size() == 0) return 0; sort(intervals.begin(), intervals.end(), cmp); int count = 1; // 记录非交叉区间的个数 - int end = intervals[0][1]; + int end = intervals[0][1]; // 记录区间分割点 for (int i = 1; i < intervals.size(); i++) { if (end <= intervals[i][0]) { end = intervals[i][1]; @@ -52,6 +100,33 @@ public: } }; ``` +* 时间复杂度:O(nlogn) ,有一个快排 +* 空间复杂度:O(1) + +大家此时会发现如此复杂的一个问题,代码实现却这么简单! + +# 总结 + +本题我认为难度级别可以算是hard级别的! + +总结如下难点: + +* 难点一:一看题就有感觉需要排序,但究竟怎么排序,按左边界排还是右边界排。 +* 难点二:排完序之后如何遍历,如果没有分析好遍历顺序,那么排序就没有意义了。 +* 难点三:直接求重复的区间是复杂的,转而求最大非重复区间个数。 +* 难点四:求最大非重复区间个数时,需要一个分割点来做标记。 + +**这四个难点都不好想,但任何一个没想到位,这道题就解不了**。 + +一些录友可能看网上的题解代码很简单,照葫芦画瓢稀里糊涂的就过了,但是其题解可能并没有把问题难点讲清楚,然后自己再没有钻研的话,那么一道贪心经典区间问题就这么浪费掉了。 + +贪心就是这样,代码有时候很简单(不是指代码短,而是逻辑简单),但想法是真的难! + +这和动态规划还不一样,动规的代码有个递推公式,可能就看不懂了,而贪心往往是直白的代码,但想法读不懂,哈哈。 + +**所以我把本题的难点也一一列出,帮大家不仅代码看的懂,想法也理解的透彻!** + +循序渐进学算法,认准「代码随想录」就够了,值得介绍给身边的朋友同学们! > 我是[程序员Carl](https://github.com/youngyangyang04),组队刷题可以找我,本文[leetcode刷题攻略](https://github.com/youngyangyang04/leetcode-master)已收录,更多[精彩算法文章](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzUxNjY5NTYxNA==&action=getalbum&album_id=1485825793120387074&scene=173#wechat_redirect)尽在:[代码随想录](https://img-blog.csdnimg.cn/20200815195519696.png),期待你的关注! diff --git a/problems/1049.最后一块石头的重量II.md b/problems/1049.最后一块石头的重量II.md index 3fd745bd..7540b4c5 100644 --- a/problems/1049.最后一块石头的重量II.md +++ b/problems/1049.最后一块石头的重量II.md @@ -1,7 +1,44 @@ -// 如何转化为01背包问题 +# 思路 + +尽量让石头分成重量相同的两堆,相撞之后剩下的石头最小,**这样就化解成01背包问题了**。 + +只不过物品的重量为store[i],物品的价值也为store[i]。 + +对应着01背包里的物品重量weight[i]和 物品价值value[i]。 + +接下来进行动规四步曲: + +* 确定dp数组以及下标的含义 + +我习惯直接使用一维dp数组,如果习惯使用二维dp数组的同学可以看这篇: + +dp[j]表示容量(这里说容量更形象,其实就是重量)为j的背包,最多可以背dp[j]这么重的石头。 + +* 确定递推公式 + +dp[j]有两个来源方向,一个是dp[j]自己,一个是dp[j - stones[i]]。 + +那么dp[j]就是取最大的:**dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);** + +一些同学可能看到这dp[j - stones[i]] + stones[i]中 又有- stones[i] 又有+stones[i],看着有点晕乎。 + +还是要牢记dp[j]的含义,要知道dp[j - stones[i]]为 容量为j - stones[i]的背包最大所背重量。 + +* dp数组如何初始化 + +既然 dp[j]中的j表示容量,那么最大容量(重量)是多少呢,就是所有石头的重量和。 + +因为提示中给出1 <= stones.length <= 30,1 <= stones[i] <= 1000,所以最大重量就是30 * 1000 + +如何初始化呢,只要因为重量都不会是负数,所以dp[j]都初始化为0就可以了,这样在递归公式dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);中dp[j]才不会初始值所覆盖。 + +* 确定遍历顺序 + +for循环遍历石头的数量嵌套一个for循环遍历背包容量,且因为是01背包,每一个物品只使用一次,所以遍历背包容量的时候要倒序。 + +具体原因我在 -尽量让石头分成,重量相同的两堆,这样就化解成 01背包问题了。 ``` class Solution { @@ -12,13 +49,14 @@ public: for (int i = 0; i < stones.size(); i++) sum += stones[i]; int target = sum / 2; for (int i = 0; i < stones.size(); i++) { - for (int j = target; j >= 0; j--) { - if (j - stones[i] >= 0) { - dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]); - } + for (int j = target; j >= stones[i]; j--) { + dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]); } } return sum - dp[target] - dp[target]; } }; ``` + +* 时间复杂度:O(m * n) , m是石头总重量,n为石头块数 +* 空间复杂度:O(m) diff --git a/problems/动态规划理论基础.md b/problems/动态规划理论基础.md index 186c4337..0cc217b9 100644 --- a/problems/动态规划理论基础.md +++ b/problems/动态规划理论基础.md @@ -1,62 +1,88 @@ +# 什么是动态规划 动态规划,英文:Dynamic Programming,简称DP,如果某一问题有很多重叠子问题,使用动态规划是最有效的。 -所以动态规划中每一个状态一定是由上一个状态推导出来的,**这一点就区分于贪心** +所以动态规划中每一个状态一定是由上一个状态推导出来的,**这一点就区分于贪心**,贪心是局部直接选最优的, + +在[关于贪心算法,你该了解这些!](https://mp.weixin.qq.com/s/O935TaoHE9Eexwe_vSbRAg)中我举了一个背包问题的例子。 + +例如:有N件物品和一个最多能被重量为W 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。**每件物品只能用一次**,求解将哪些物品装入背包里物品价值总和最大。 + +动态规划中dp[j]是又dp[j-weight[i]]推导出来的。 + +但如果是贪心呢,dp[j]每次选一个最大的或者最小的就完事了。 + +所以贪心解决不了动态规划的问题,这也是最大的区别。 + +很多讲解动态规划的文章都会讲最优子结构啊和重叠子问题啊这些,这些东西都是教科书的上定义,晦涩难懂而且不实用。 + +大家只要知道,动规是由前一个状态推导出来的,而贪心是局部直接算最优的,就够用了。 + +对于上述提到的背包问题,后序会详细讲解。 + + +# 动态规划的解题步骤 题目的时候,很多同学会陷入一个误区,就是以为把状态转移公式背下来,照葫芦画瓢改改,就开始写代码,甚至把题目AC之后,都不太清楚dp[i]表示的是什么。 这就是一种朦胧的状态,然后就把题给过了,遇到稍稍难一点的,可能直接就不会了,然后看题解,然后继续照葫芦画瓢陷入这种恶性循环中。 -关于状态转移公式, +状态转移公式(递推公式)是很重要,但动规不仅仅只有递推公式。 -对于动态规划问题,我将拆解为如下四步曲,这四步都搞清楚了,才能说把动态规划真的掌握了! +**对于动态规划问题,我将拆解为如下四步曲,这四步都搞清楚了,才能说把动态规划真的掌握了!** * 确定dp数组以及下标的含义 -* dp数组如何初始化 * 确定递推公式 +* dp数组如何初始化 * 确定遍历顺序 +一些同学可能想为什么要先确定递推公式,然后在考虑初始化呢? + +因为一些情况是递推公式决定了dp数组要输入初始化! + 后面的讲解中我都是围绕着这四个点来经行讲解。 可能刷过动态规划题目的同学可能都知道递推公式的重要性,感觉确定了递推公式这道题目就解出来了。 -其实 确定递推公式 仅仅是解题里的一步而且, dp数组的初始化 以及确定遍历顺序,都非常重要, +其实 确定递推公式 仅仅是解题里的一步而且,dp数组以及下标的含义, dp数组的初始化 以及确定遍历顺序,都非常重要。 -**很多同学搞不清楚dp数组应该如何初始化,或者遍历的顺序,以至于记下来公式,但写的程序怎么改都通过不了**。 +后序的讲解的大家就会发现其重要性。 -# 动态规划如何debug +很多同学甚至知道递推公式,但搞不清楚dp数组应该如何初始化,或者正确的遍历顺序,以至于记下来公式,但写的程序怎么改都通过不了。 + +# 动态规划应该如何debug 平时我自己写的时候也经常出问题,**找问题的最好方式就是把dp数组打印出来,看看究竟是不是按照自己思路推导的!** +一些同学对于dp的学习是上来就是俯视类型的,总是想一下子全盘接纳,一鼓作气写出代码,如果代码能通过万事大吉,通过不了的话就凭感觉改一改。 -# 背包三讲 +这是一个很不好的习惯! -背包九讲其实看起来还是有点费劲的,而且都是伪代码理解起来吃力 - +**做动规的题目,写代码之前一定要把状态转移在dp数组的上具体情况模拟一遍,心中有数,确定最后推出的是想要的结果**。 + +然后在写代码,如果代码没通过就打印dp数组,看看是不是和自己推导的哪里不一样。 + +如果和自己模拟推导的一样,那么就是自己的递归公式有问题。 + +如果和自己模拟推导的不一样,那么就是代码实现细节有问题。 + +**这样才是一个完整的思考过程,而不是一旦代码出问题,就毫无头绪的东改改西改改,最后过不了,或者说是稀里糊涂的过了**。 + +我来举一个例子:一些同学可能代码通过不了,都会把代码抛到出来问:我这里代码都已经和题解一模一样的,为什么通过不了呢? + +发出这样的问题之前,其实可以自己先思考这三个问题: + +* 这道题目我推导状态转移公式了么? +* 我打印dp数组的日志了么? +* 打印出来了dp数组和我想的一样么? + +**如果这灵魂三问自己都做到了,基本上这道题目也就解决了**,或者更清晰的知道自己究竟是哪一点不明白,是状态转移不明白,还是实现代码不知道该怎么写,还是不理解遍历dp数组的顺序。 + +然后在问问题,目的性就很强了,回答问题的同学也可以快速知道提问者的疑惑了。 + +# 动态规划可以解决哪一类问题 -# 完全背包 -有N 种物品和一个容量为V 的背包,每种物品都有无限件可用。放入第i种 物品的耗费的空间是Ci ,得到的价值是Wi 。求解:将哪些物品装入背包,可使 这些物品的耗费的空间总和不超过背包容量,且价值总和最大。 - -这个问题非常类似于01背包问题,所不同的是每种物品有无限件 - - -首先想想为什么01背包中要按照v递减的次序来 循环。让v递减是为了保证第i次循环中的状态F [i, v]是由状态F [i − 1, v − Ci]递 推而来。换句话说,这正是为了保证每件物品只选一次,保证在考虑“选入 第i件物品”这件策略时,依据的是一个绝无已经选入第i件物品的子结果F [i − 1, v − Ci]。而现在完全背包的特点恰是每种物品可选无限件,所以在考虑“加 选一件第i种物品”这种策略时,却正需要一个可能已选入第i种物品的子结 果F [i, v − Ci],所以就可以并且必须采用v递增的顺序循环。这就是这个简单的 -程序为何成立的道理。 - - -值得一提的是,上面的伪代码中两层for循环的次序可以颠倒。这个结论有可能会带来算法时间常数上的优化。(可能说的就是组合或者排列了) - -# 多重背包 - -有N种物品和一个容量为V 的背包。第i种物品最多有Mi件可用,每件耗费 的空间是Ci ,价值是Wi 。求解将哪些物品装入背包可使这些物品的耗费的空间 总和不超过背包容量,且价值总和最大。 - -这题目和完全背包问题很类似。基本的方程只需将完全背包问题的方程略 微一改即可。 - - -# 总结 - -后台回复:背包九讲 就可以获得pdf diff --git a/problems/导读.md b/problems/导读.md index f852cd0d..906b614d 100644 --- a/problems/导读.md +++ b/problems/导读.md @@ -1,6 +1,6 @@ 很多刚刚关注「代码随想录」的录友,表示想从头开始打卡学习。 -**打卡方式**就是在文章留言区记录:第n天打卡+自己的总结。很多录友都正在从头开始打卡,看看留言就知道了,你并不孤独,哈哈。 +**打卡方式**就是在文章留言区记录:第n天打卡(或者第n次打卡)+自己的总结。很多录友都正在从头开始打卡,看看留言就知道了,你并不孤独,哈哈。 **以下是我整理的文章列表,每个系列都排好了顺序,文章顺序即刷题顺序,这是全网最详细的刷题顺序了,所以录友们挨个看就OK!** @@ -17,11 +17,13 @@ * 求职 * [程序员的简历应该这么写!!(附简历模板)](https://mp.weixin.qq.com/s/nCTUzuRTBo1_R_xagVszsA) - * [BAT级别技术面试流程和注意事项都在这里了](https://mp.weixin.qq.com/s/815qCyFGVIxwut9I_7PNFw) - * [深圳原来有这么多互联网公司,你都知道么?](https://mp.weixin.qq.com/s/Yzrkim-5bY0Df66Ao-hoqA) - * [北京有这些互联网公司,你都知道么?]() + * [互联网大厂技术面试流程和注意事项](https://mp.weixin.qq.com/s/815qCyFGVIxwut9I_7PNFw) + * [北京有这些互联网公司,你都知道么?](https://mp.weixin.qq.com/s/BKrjK4myNB-FYbMqW9f3yw) * [上海有这些互联网公司,你都知道么?]() - * [成都有这些互联网公司,你都知道么?]() + * [深圳有这些互联网公司,你都知道么?](https://mp.weixin.qq.com/s/3VJHF2zNohBwDBxARFIn-Q) + * [广州有这些互联网公司,你都知道么?](https://mp.weixin.qq.com/s/Ir_hQP0clbnvHrWzDL-qXg) + * [成都有这些互联网公司,你都知道么?](https://mp.weixin.qq.com/s/Y9Qg22WEsBngs8B-K8acqQ) + * 算法性能分析 * [关于时间复杂度,你不知道的都在这里!](https://mp.weixin.qq.com/s/LWBfehW1gMuEnXtQjJo-sw) @@ -167,6 +169,14 @@ * [贪心算法:K次取反后最大化的数组和](https://mp.weixin.qq.com/s/dMTzBBVllRm_Z0aaWvYazA) * [本周小结!(贪心算法系列二)](https://mp.weixin.qq.com/s/RiQri-4rP9abFmq_mlXNiQ) * [贪心算法:加油站](https://mp.weixin.qq.com/s/aDbiNuEZIhy6YKgQXvKELw) + * [贪心算法:分发糖果](https://mp.weixin.qq.com/s/8MwlgFfvaNYmjGwjuMlETQ) + * [贪心算法:柠檬水找零](https://mp.weixin.qq.com/s/0kT4P-hzY7H6Ae0kjQqnZg) + * [贪心算法:根据身高重建队列](https://mp.weixin.qq.com/s/-2TgZVdOwS-DvtbjjDEbfw) + * [本周小结!(贪心算法系列三)](https://mp.weixin.qq.com/s/JfeuK6KgmifscXdpEyIm-g) + * [贪心算法:根据身高重建队列(续集)](https://mp.weixin.qq.com/s/K-pRN0lzR-iZhoi-1FgbSQ) + * [贪心算法:用最少数量的箭引爆气球](https://mp.weixin.qq.com/s/HxVAJ6INMfNKiGwI88-RFw) + + (持续更新.....) diff --git a/problems/背包理论基础01背包-2.md b/problems/背包理论基础01背包-2.md index f12a29e7..cb657d51 100644 --- a/problems/背包理论基础01背包-2.md +++ b/problems/背包理论基础01背包-2.md @@ -129,3 +129,6 @@ void test_1_wei_bag_problem() { } ``` +# 总结 + +我个人倾向于使用一维dp数组的写法,比较直观简洁,所以在后面背包问题的讲解中,我都直接使用一维dp数组来进行推导。 diff --git a/problems/背包问题理论基础.md b/problems/背包问题理论基础.md new file mode 100644 index 00000000..a2264a87 --- /dev/null +++ b/problems/背包问题理论基础.md @@ -0,0 +1,33 @@ + +# 背包问题 + +背包九讲其实看起来还是有点费劲的,而且都是伪代码理解起来吃力 + +# 完全背包 + +有N件物品和一个最多能背重量为W的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。**每件物品都有无限个(也就是可以放入背包多次)**,求解将哪些物品装入背包里物品价值总和最大。 + + +完全背包和01背包问题唯一不同的地方就是,每种物品有无限件。 + +**终点讲解两个for顺序的问题,完全背包就可以换顺序了,因为不需要从后向前遍历了**。 + + +程序为何成立的道理。 + + +值得一提的是,上面的伪代码中两层for循环的次序可以颠倒。这个结论有可能会带来算法时间常数上的优化。(可能说的就是组合或者排列了) + +# 多重背包 + +有N种物品和一个容量为V 的背包。第i种物品最多有Mi件可用,每件耗费 的空间是Ci ,价值是Wi 。求解将哪些物品装入背包可使这些物品的耗费的空间 总和不超过背包容量,且价值总和最大。 + +这题目和完全背包问题很类似。基本的方程只需将完全背包问题的方程略 微一改即可。 + +# 总结 + + + +# 总结 + +后台回复:背包九讲 就可以获得pdf