动态规划Ⅳ:分割整数
动态规划Ⅳ:分割整数
动态规划题目类型 & 做题思路总览:动态规划解题套路 & 题型总结 & 思路讲解
文章目录
-
四、分割整数
1. 凑零钱问题
凑零钱:给你 k 种面值的硬币,面值分别为 c1, c2 … ck,每种硬币的数量无限,再给一个总金额 amount,问你最少需要几枚硬币凑出这个金额,如果不可能凑出,算法返回 -1。LeetCode 直达
乍一看好像是贪心问题,只要不断加上最大面值的硬币即可。但是这样没有考虑到一类情况,就是目标金额不能整除所给面值。所以必须用动态规划来做。
解题思路:明确 “状态” -> 明确 dp 数组 / 函数的定义 -> 明确 “选择” -> 寻找状态之间的关系 -> 明确 base case
- 明确状态:状态就是原问题和子问题中**会变化的变量**。在硬币数量无限的条件下,唯一的状态就是目标金额- DP 数组:当目标金额为
i
时,至少需要dp[i]
枚硬币才能凑出;对于凑不出的情况,对应的 dp 值为 -1- 明确选择:对于给定的硬币面值,我们的选择就是该列表里不同的面值- 寻找状态间关系:由于硬币面值是给定的,计算状态dp[i]
时需要穷举出选择每个面值的情况,最终取一个硬币个数最小的情况- 确定 base case:目标金额 = 0 时,不需要硬币,返回 0 ``` class Solution: def coinChange(self, coins: List[int], amount: int) -> int:# 初始化一个 n+1 大小的数组,第0位表示目标金额为0 dp = [amount + 1 for _ in range(amount + 1)] dp[0] = 0 for i in range(amount + 1): for coin in coins: # 尝试所有的硬币面额 if i < coin: continue # 无解 dp[i] = min(dp[i], dp[i - coin] + 1) return dp[-1] if dp[-1] != amount+1 else -1
上面的思路是我看大佬的!想秃了脑袋也没想出来,抄作业了抄作业了。 PS:为什么将 dp 数组初始化为 `目标金额+1`:因为当目标金额为 n 时,最多需要 n 枚硬币来凑(硬币面额 = 1),初始化为 n + 1,便于循环中找到最小值。 ### 2. 分割整数的最大乘积 **分割整数的最大乘积**:给定一个正整数 n,将其拆分为至少两个正整数的和,并使这些整数的乘积最大化。 返回你可以获得的最大乘积。[LeetCode 直达](https://leetcode-cn.com/problems/integer-break/) **解题思路**:明确 “状态” -> 明确 dp 数组 / 函数的定义 -> 明确 “选择” -> 寻找状态之间的关系 -> 明确 base case - **明确状态**:变量是当前目标值 `i` 拆分后的最大乘积- **DP 数组**:dp 数组中每个元素 `dp[i]` 表示 `i` 拆分后的最大乘积- **明确选择**:每个数 `i` 都可以拆分为多种情况,选择这些情况中乘积最大的一种- **寻找状态间的关系**:这个关系就比较繁琐了,对任意数字 `i`,它可以拆分成 i-1和1,i-2和2,…,⌈i/2⌉和⌊i/2⌋。需要寻找这些划分组合中最大的乘积结果- **明确 base case**:当 n = 2 时,只能划分为 1 + 1,dp[1] = 1。至于 n = 1 时,按理说这是个非法输入,但是初始化为 0 或者 1 都可以
class Solution: def integerBreak(self, n: int) -> int: if n <= 1: return 1 dp = [0 for _ in range(n)] dp[1] = 1 for i in range(2, n): m = math.ceil(i / 2) for j in range(m): dp[i] = max(dp[i], max(dp[j], (j+1)) * max(dp[i-j-1], (i-j))) return dp[-1]
自己挣扎了好久才做出来(太菜了!) 解释一下状态转移方程怎么得来的(就是常规思路,也许有更好的方法,但是我比较笨,不要介意哈哈哈):`dp[i] = max(dp[i], max(dp[j], (j+1)) * max(dp[i-j-1], (i-j)))`,假设 n = 10,那么可以列出从 1 到 10 及对应的 dp 状态值: |num|1|2|3|4|5|6|7|8|9|10 |------ |index|0|1|2|3|4|5|6|7|8|9 |dp|0|1|2|4|6|9|12|18|27|36 **ps-1**:为了方便描述,把数 k 拆分后能取得的最大成乘积称作 <mark>拆分最大积</mark>。<br> **ps-2**:下面的例子和实际代码会有下标(index)的细小差别,就不做详细区分啦,写代码注意就行。 目标值是 10,我们自底向上进行推进: - 首先是 1 和 2,其拆分后最大乘积分别初始化为 0 和 1- i = 3:3 可以拆分为 1 + 2 或 1 + 1 + 1,明显是 1 * 2 比较大,所以 3 的状态就是 2。那么是否还需要单独算一下 3 个 1 的情况呢?当然不需要啦,因为三个 1 可以看作是 2 拆分为了 1 和 1,即要么我们选择 1 和 2 本身相乘,要么选择 1 和 2 的拆分最大积相乘,即 `max(1*2, 1*1) = 2`- i = 8:8 可以拆分为 7 + 1,6 + 2,5 + 3,4 + 4,为了求得这些组合的最大值,需要依次计算。对于 6+2 组合,观察表格发现 6 的 dp 值为 9,2 的 dp 值为 1,为了使乘积最大,我们选择 “用 6 的 dp 值 * 2 本身”,即 `9 * 2 = 18`,这样做其实就相当于把 6 拆分为了 3 + 3 综上,对于数 k,将它先拆分成两个数之和 m + n,m、n 可能是本身大于其 dp 状态(比如 3 的 dp = 2),也可能是 dp 状态大于其本身(比如 9 的 dp = 27)。我们总是选择较大的数进行相乘,如果选择数字本身(m、n),相当于选择不拆分;如果选择状态(dp[m], dp[n]),相当于拆分其为更小的数。 另外又去看了大佬的题解,发现这还是一道数学题!在 [这个链接](https://leetcode-cn.com/problems/integer-break/solution/343-zheng-shu-chai-fen-tan-xin-by-jyd/) 自己看看吧~ 其实就是贪心算法,但是贪心算法也是动规的特例不是~~
贪心代码
class Solution: def integerBreak(self, n: int) -> int: if n <= 3: return n - 1 a, b = n // 3, n % 3 if b == 0: return int(math.pow(3, a)) if b == 1: return int(math.pow(3, a - 1) 4) return int(math.pow(3, a) 2)
时隔两个月,再次遇到这题,几分钟就想到了思路,写代码也很快:
class Solution: def integerBreak(self, n: int) -> int: if n < 2: return None dp = [0 for _ in range(n+1)] dp[1] = dp[2] = 1 for i in range(3, n+1): for j in range(1, int(i/2)+1): dp[i] = max(dp[i], max(j, dp[j]) * max(i-j, dp[i-j])) return dp[-1]
### 3. 按平方数来分割整数 **完全平方数**:给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, …)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。[LeetCode 直达](https://leetcode-cn.com/problems/perfect-squares/) - 明确状态:变量是当前目标值 `i` 所需要的 **最少完全平方数个数**- DP 数组:数组中元素 `dp[i]` 表示目标值 `i` 需要的 **最少完全平方数个数**- 明确选择:对于任意数字 `i`,其可以由全 1 的完全平方数组成(个数就为 i),也可以寻找是否存在更大的完全平方数 `j*j`,此时构成 `i` 的完全平方数个数就是构成 `i - j * j` 的完全平方数个数 + 1,因此不难看出,我们需要一个二重循环,内层遍历可选的 `j`- 状态间的关系:对于数字 `i`,其分解的完全平方数个数最差情况下就是全 1,或者取更小值 `dp[i - j * j] + 1`,所以可以通过循环取得最优解:`dp[i] = min(dp[i], dp[i - j * j] + 1)`- 确定 base case:创建 n + 1 大小的 dp 数组,其中 `dp[0] = 0` 为虚构元素用于简化逻辑,dp 中每个元素初始化为 `i`,代表最差情况下的个数。 **时间复杂度**: O ( n 2 ) O(n^2) O(n2)<br> **空间复杂度**: O ( n ) O(n) O(n),dp 数组需要额外存储空间
class Solution: def numSquares(self, n: int) -> int: dp = [i for i in range(n+1)] for i in range(1, n+1): for j in range(1, i): if i - j j >= 0: dp[i] = min(dp[i], dp[i - j j] + 1) return dp[-1]
注:Python解法在 Leetcode 提交会超时。 另外看到解析的数学解法:四平方定理。 **四平方定理**:任何一个正整数都可以表示成不超过四个整数的平方之和。 推论:满足四数平方和定理的数n(四个整数的情况),必定满足 n=4^a(8b+7)。
class Solution: def numSquares(self, n: int) -> int: while n % 4 == 0: n /= 4 if n % 8 == 7: return 4 a = 0 while a2 <= n: b = int((n - a2)0.5) if a2 + b**2 == n: return (not not a) + (not not b) a += 1 return 3
### 4. 分割整数构成字母字符串 **解码方法**:一条包含字母 A-Z 的消息通过以下方式进行了编码:
'A' -> 1 'B' -> 2 ... 'Z' -> 26
给定一个只包含数字的非空字符串,请计算解码方法的总数。[LeetCode 直达](https://leetcode-cn.com/problems/decode-ways/description/) - 明确状态:变量为 “解码方式的数量”- dp 数组:对于以第 `i` 个数字字符为结尾的字符串,`dp[i]` 表示当前可能的解码数量- 明确选择:对于第 `i` 个数字字符,其可以单独作为一个字符进行解码,也可以判断是否可以和前一个数字字符合成一个字符进行解码,条件是组合起来不能超过 26- 状态间的关系:如果第 `i` 个数字字符选择了单独进行解码,实际对解码方式总数没有贡献,`dp[i] = dp[i-1]`;如果选择和前一个数字联合解码,则 `dp[i] = dp[i-1] + dp[i-2]`。- 确定 base case:只有一个数字字符时(即 s[0]),如果是 0 则解码失败直接 return 0,否则仅有一种解码方式,即 `dp[0] = 1` 另外,对于数字 0 要进行特别判断,0 不能单独解码,也不能作为联合字符的前缀解码,只能作为联合字符的后缀进行解码。 【对 0 的处理】在每个位置 `i` 进行判断 - 如果 `s[i] == '0'`,只能作为联合码的后缀,此时若 `s[i-1] == '1' or '2'`,则 `dp[i] = dp[i-2]`,否则 return 0- 如果 `s[i] != '0'`,且 `s[i-1] != '0'`,并且 `s[i-1]` 与 `s[i]` 可以联合解码,则 `dp[i] = dp[i-1] + dp[i-2]`- 除此之外的情况,`s[i]` 只能单独解码,此时 `dp[i] = dp[i-1]` **时间复杂度**: O ( n ) O(n) O(n)<br> **空间复杂度**: O ( n ) O(n) O(n)
class Solution: def numDecodings(self, s: str) -> int: s = list(s) if s[0] == '0': return 0 n = len(s) dp = [0 for _ in range(n)] dp[0] = 1 for i in range(1, n): if s[i] == '0': if s[i-1] == '1' or s[i-1] == '2': dp[i] = dp[i-2] if i-2 >= 0 else 1 else: return 0 elif s[i-1] != '0' and int(s[i-1])*10 + int(s[i]) <= 26: dp[i] = dp[i-1] + dp[i-2] if i - 2 >= 0 else dp[i-1] + 1 else: dp[i] = dp[i-1] return dp[-1]
为了节省空间,可以使用滚动变量保存前置状态:<br> **时间复杂度**: O ( n ) O(n) O(n)<br> **空间复杂度**: O ( 1 ) O(1) O(1)
class Solution: def numDecodings(self, s: str) -> int: s = list(s) if s[0] == '0': return 0 n, pre, cur = len(s), 1, 1 for i in range(1, n): if s[i] == '0': if s[i-1] == '1' or s[i-1] == '2': cur = pre else: return 0 elif s[i-1] != '0' and int(s[i-1])*10 + int(s[i]) <= 26: pre, cur= cur, cur + pre else: pre = cur return cur
```
- 明确状态:状态就是原问题和子问题中**会变化的变量**。在硬币数量无限的条件下,唯一的状态就是目标金额- DP 数组:当目标金额为