From 194abf83f18a7fdbf555be61492da52700fc1ea1 Mon Sep 17 00:00:00 2001 From: youngyangyang04 <826123027@qq.com> Date: Fri, 25 Dec 2020 09:27:53 +0800 Subject: [PATCH] Update --- README.md | 3 ++ problems/0070.爬楼梯.md | 24 ++++++++--- problems/0139.单词拆分.md | 42 +++++++++++++++---- problems/0279.完全平方数.md | 25 ++++++++++++ problems/0322.零钱兑换.md | 6 +-- problems/0377.组合总和Ⅳ.md | 61 ++++++++++++++++++---------- problems/0435.无重叠区间.md | 2 +- problems/0474.一和零.md | 3 ++ problems/0518.零钱兑换II.md | 61 +++++++++++++++------------- problems/背包问题理论基础.md | 3 ++ 10 files changed, 162 insertions(+), 68 deletions(-) diff --git a/README.md b/README.md index e2da4680..a862523a 100644 --- a/README.md +++ b/README.md @@ -218,6 +218,9 @@ * [本周小结!(贪心算法系列三)](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) + * [贪心算法:无重叠区间](https://mp.weixin.qq.com/s/oFOEoW-13Bm4mik-aqAOmw) + * [贪心算法:划分字母区间](https://mp.weixin.qq.com/s/pdX4JwV1AOpc_m90EcO2Hw) + * [贪心算法:合并区间](https://mp.weixin.qq.com/s/royhzEM5tOkUFwUGrNStpw) * 动态规划 diff --git a/problems/0070.爬楼梯.md b/problems/0070.爬楼梯.md index cfbb1af6..bb98afca 100644 --- a/problems/0070.爬楼梯.md +++ b/problems/0070.爬楼梯.md @@ -25,22 +25,32 @@ public: 既然这么简单为什么还要讲呢,其实本题稍加改动就是一道面试好题,如果每次可以爬 1 或 2或3或者m 个台阶呢,走到楼顶有几种方法? - - * 确定dp数组以及下标的含义 dp[i]:爬到有i个台阶的楼顶,有dp[i]种方法 * 确定递推公式 -dp[i]有几种来源,dp[i - 1],dp[i - 2] +dp[i]有几种来源,dp[i - 1],dp[i - 2],dp[i - 3] 等等,即:dp[i - j] + +那么递推公式为:dp[i] += dp[i - j] * dp数组如何初始化 + +既然递归公式是 dp[i] += dp[i - j],那么dp[0] 一定为1,dp[0]是递归中一切数值的基础所在,如果dp[0]是0的话,其他数值都是0了。 + +下标非0的dp[i]初始化为0,因为dp[i]是靠dp[i-j]累计上来的,dp[i]本身为0这样才不会影响结果 + * 确定遍历顺序 -dp里求排列,1 2 步 和 2 1 步都是上三个台阶,但不一样! +这是背包里求排列问题,即:1 2 步 和 2 1 步都是上三个台阶,但是这两种方法不! -这是求排列 +所以需将target放在外循环,将nums放在内循环。 + +每一步可以走多次,说明这是完全背包,内循环需要从前向后遍历。 + + +C++代码如下: ``` class Solution { public: @@ -48,7 +58,7 @@ public: vector dp(n + 1, 0); dp[0] = 1; for (int i = 1; i <= n; i++) { - for (int j = 1; j <= 2; j++) { + for (int j = 1; j <= m; j++) { if (i - j >= 0) dp[i] += dp[i - j]; } } @@ -57,6 +67,8 @@ public: }; ``` +代码中m表示最多可以爬m个台阶,代码中把m改成2就是本题70.爬楼梯的代码了。 + # 总结 如果我来面试的话,我就会想给候选人出一个 本题原题,看其表现,如果顺利写出来,进而在要求每次可以爬[1 - m]个台阶应该怎么写。 diff --git a/problems/0139.单词拆分.md b/problems/0139.单词拆分.md index aed68179..31d67ac5 100644 --- a/problems/0139.单词拆分.md +++ b/problems/0139.单词拆分.md @@ -1,8 +1,4 @@ -// 如何往 完全背包上靠? -// 用多次倒是可以往 完全背包上靠一靠 -// 和单词分割的问题有点像 - [回溯算法:分割回文串](https://mp.weixin.qq.com/s/Pb1epUTbU8fHIht-g_MS5Q) 回溯法代码: @@ -68,11 +64,40 @@ public: }; ``` +# 背包 -得好好分析一下,完全背包和01背包,这个对于刷leetcode太重要了 +* 确定dp数组以及下标的含义 -注意这里要空出一个 dp[0] 来做起始位置 -``` +dp[i] : 字符串长度为i的话,dp[i]为true,表示可以拆分为一个或多个在字典中出现的单词 + +* 确定递推公式 + +如果确定dp[j] 是true,且 [j, i] 这个区间的子串出现在字典里,那么dp[i]一定是true。(j < i ) + +所以递推公式是 if([j, i] 这个区间的子串出现在字典里 && dp[j] ==true) 那么 dp[i] = true + + +* dp数组如何初始化 + +从递归公式中可以看出,dp[i] 的状态依靠 dp[j]是否为true,那么dp[0]就是递归的根基,dp[0]一定要为true,否则递归下去后面都都是false了。 + +同时也表示如果字符串为空的话,说明出现在字典里。 + +下标非0的dp[i]初始化为false,只要没有被覆盖说明都是不可拆分为一个或多个在字典中出现的单词。 + +* 确定遍历顺序 + +题目中说是拆分为一个或多个在字典中出现的单词,所以这是完全背包。 + +同时也说明出现的单词集合是组合还是排列,并不在意,最终要求的是 是否都出现过。 + +所以本题使用求排列的方式,还是求组合的方式都可以。 + +我采用的求排列的方式,所以遍历顺序:target放在外循环,将nums放在内循环。内循环从前到后。 + +C++代码如下: + +```C++ class Solution { public: bool wordBreak(string s, vector& wordDict) { @@ -93,4 +118,5 @@ public: } }; ``` -时间复杂度起始是O(n^3),因为substr返回子串的副本是O(n)的复杂度(n是substring的长度) +* 时间复杂度:O(n^3),因为substr返回子串的副本是O(n)的复杂度(这里的n是substring的长度) +* 空间复杂度:O(n) diff --git a/problems/0279.完全平方数.md b/problems/0279.完全平方数.md index 9903df5c..21178942 100644 --- a/problems/0279.完全平方数.md +++ b/problems/0279.完全平方数.md @@ -1,7 +1,10 @@ 没有问你组合方式,而是问你最小个数 +和322 一个套路 + // 组合的逻辑 ``` +// 版本一 class Solution { public: int numSquares(int n) { @@ -23,9 +26,31 @@ public: }; ``` +优化一下代码,可以不用预先用sum数组来装i * i,但是版本一更清晰一些,代码如下: +``` +// 版本二 +class Solution { +public: + int numSquares(int n) { + vector dp(n + 1, INT_MAX); + dp[0] = 0; + for (int i = 1; i * i <= n; i++) { + for (int j = 1; j <= n; j++) { + if (j - i * i >= 0 && dp[j - i * i ] != INT_MAX) { + dp[j] = min(dp[j - i * i ] + 1, dp[j]); + } + } + } + return dp[n]; + } +}; + +``` + // 排列的逻辑 ``` +// 版本三 class Solution { public: int numSquares(int n) { diff --git a/problems/0322.零钱兑换.md b/problems/0322.零钱兑换.md index e3d5d386..c308c7ab 100644 --- a/problems/0322.零钱兑换.md +++ b/problems/0322.零钱兑换.md @@ -5,13 +5,13 @@ dp[j]:凑足总额为j所需钱币的最少个数为dp[j] * 确定递推公式 -得到dp[j](有考虑coins[i]),有两个来源,一个是dp[j - coins[i]],一个是dp[j] (没有考虑coins[i])自己。 +得到dp[j](考虑coins[i]),只有一个来源,dp[j - coins[i]](没有考虑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] 要去所有 dp[j - coins[i]] + 1 中最小的。 -dp[j] = min(dp[j - coins[i]] + 1, dp[j]); +递推公式:dp[j] = min(dp[j - coins[i]] + 1, dp[j]); * dp数组如何初始化 diff --git a/problems/0377.组合总和Ⅳ.md b/problems/0377.组合总和Ⅳ.md index 0db1d908..6849bb3b 100644 --- a/problems/0377.组合总和Ⅳ.md +++ b/problems/0377.组合总和Ⅳ.md @@ -1,5 +1,41 @@ +# 思路 -和之前个回溯法的各个总和串起来一波,本题用回溯稳稳的超时 +本题题目描述说是求组合,但又说是可以元素相同顺序不同的组合算两个组合,其实就是求排列! + +弄清什么是组合,什么是排列很重要。 + +大家在学习回溯算法系列的时候,一定做过这两道题目[回溯算法:39.组合总和](https://mp.weixin.qq.com/s/FLg8G6EjVcxBjwCbzpACPw)和[回溯算法:40.组合总和II](https://mp.weixin.qq.com/s/_1zPYk70NvHsdY8UWVGXmQ) + +大家会感觉很像,但其本质是本题求的是排列总和,而且仅仅是求排列总和的个数,并不是把所有的排列都列出来。 + +如果本题要把排列都列出来的话,只能使用回溯算法爆搜。 + + +* 确定dp数组以及下标的含义 + +dp[i]: 凑成目标正整数为i的组合个数为dp[i] + +* 确定递推公式 + +dp[i](考虑nums[j])可以由 dp[i - nums[j]](不考虑nums[j]) 推导出来。 + +因为只要得到nums[j],排列个数dp[i - nums[j]],就是dp[i]的一部分。 + +所以递归公式: dp[i] += dp[i - nums[j]]; + +* dp数组如何初始化 + +因为递推公式dp[i] += dp[i - nums[j]]的缘故,dp[0]要初始化为1,这样递归其他dp[i]的时候才会有数值基础。 + +非0下标的dp[i]初始化为0,这样才不会影响dp[i]累加所有的dp[i-nums[j]] + +* 确定遍历顺序 + +个数可以不限使用,这是一个完全背包,且得到的集合是排列(需要考虑元素之间的顺序)。 + +所以将target放在外循环,将nums放在内循环,内循环从前到后遍历。 + +C++代码如下: ``` class Solution { @@ -19,28 +55,9 @@ public: }; ``` +C++测试用例有超过两个树相加超过int的数据,所以需要在if里加上dp[i] < INT_MAX - dp[i - num]。 -C++测试用例有超过两个树相加超过int的数据 -超限的情况 +但java就不用考虑这个限制,我理解java里的int也是四个字节吧,也有可能leetcode后台对不同语言的测试数据不一样。 -一些题解会直接用ull usigned long long -java 也是四个字节,理论上没有差别,可能后台java和C++测试用例不同,bug..... -``` -class Solution { -public: - int combinationSum4(vector& nums, int target) { - vector dp(target + 1, 0); - dp[0] = 1; - for (int i = 0; i <= target; i++) { - for (int num : nums) { - if (i - num >= 0 && dp[i] < INT_MAX - dp[i - num]) { - dp[i] += dp[i - num]; - } - } - } - return dp[target]; - } -}; -``` diff --git a/problems/0435.无重叠区间.md b/problems/0435.无重叠区间.md index 0637ab5b..a1579858 100644 --- a/problems/0435.无重叠区间.md +++ b/problems/0435.无重叠区间.md @@ -157,7 +157,7 @@ public: }; ``` -这里按照 作弊案件遍历,或者按照右边界遍历,都可以AC,具体原因我还没有仔细看,后面有空再补充。 +这里按照 左区间遍历,或者按照右边界遍历,都可以AC,具体原因我还没有仔细看,后面有空再补充。 ```C++ class Solution { public: diff --git a/problems/0474.一和零.md b/problems/0474.一和零.md index 29a81a0a..3d8359cd 100644 --- a/problems/0474.一和零.md +++ b/problems/0474.一和零.md @@ -8,6 +8,9 @@ 搞不懂 leetcode后台是什么牛逼的编译器,初始化int dp[101][101] = {0}; 可以 ,int dp[101][101];就不行,有其他默认值,坑死。 代码我做了实验,后台会拿findMaxForm,运行两次,取第二次的结果,dp有上次记录的数值。 +本题其实不是多重背包问题,还是一个01背包问题,m 和 n 可以理解是一个二维的背包,而不同长度的字符串就是不同大小的待装物品。 + + ``` class Solution { public: diff --git a/problems/0518.零钱兑换II.md b/problems/0518.零钱兑换II.md index b60829a3..41019f82 100644 --- a/problems/0518.零钱兑换II.md +++ b/problems/0518.零钱兑换II.md @@ -1,38 +1,43 @@ -// 计算有多少种方式 -// 完全背包 for循环的顺序啊,先是哪个for 后是哪个for定不下来 +# 思路 + +这是一道典型的背包问题,一看到钱币数量不限,就知道这是一个完全背包 + +那么用动规四步曲来进行分析: + +* 确定dp数组以及下标的含义 + +dp[j]:凑成总金额j的货币组合数为dp[j] + +* 确定递推公式 + +dp[j] (考虑coins[i]的组合总和) 就是所有的dp[j - coins[i]](不考虑coins[i])相加。 + +所以递推公式:dp[j] += dp[j - coins[i]]; + +* dp数组如何初始化 + +首先dp[0]一定要为1,dp[0]=1是 递归公式的基础。 + +下标非0的dp[j]初始化为0,这样累计加dp[j - coins[i]]的时候才不会影响真正的dp[j] + +* 确定遍历顺序 + +本题是完全背包,而且求的是组合,不涉及钱币的顺序 + +所以 nums放在外循环,target在内循环,内循环正序遍历。 + +C++代码如下: -排列 ``` class Solution { public: int change(int amount, vector& coins) { - int dp[50001] = {0}; + vector dp(amount + 1, 0); dp[0] = 1; - for (int i = 0; i <= amount; i++) { - for (int j = 0; j < coins.size(); j++) { // 这是组合把??? - if (i - coins[j] >= 0) dp[i] += dp[i - coins[j]]; - } - for (int j = 0; j <= amount; j++) { - cout << dp[j] << " "; - } - cout << endl; - } - return dp[amount]; - } -}; -``` - -这个才是组合,本题的题解, -``` -class Solution { -public: - int change(int amount, vector& coins) { - int dp[50001] = {0}; - dp[0] = 1; - for (int i = 0; i < coins.size(); i++) { // 一个钱币只在序列里出现一次 - for (int j = 0; j <= amount; j++) { - if (j - coins[i] >= 0) dp[j] += dp[j - coins[i]]; + for (int i = 0; i < coins.size(); i++) { + for (int j = coins[i]; j <= amount; j++) { + dp[j] += dp[j - coins[i]]; } } return dp[amount]; diff --git a/problems/背包问题理论基础.md b/problems/背包问题理论基础.md index a2264a87..3e2d77ab 100644 --- a/problems/背包问题理论基础.md +++ b/problems/背包问题理论基础.md @@ -24,6 +24,9 @@ 这题目和完全背包问题很类似。基本的方程只需将完全背包问题的方程略 微一改即可。 +与 0-1 背包的区别在于每种物品 y 有 k 个,而非 1 个 + +力扣上面没有多重背包的题目。 # 总结