动态规划Ⅳ:分割整数

动态规划Ⅳ:分割整数

动态规划题目类型 & 做题思路总览动态规划解题套路 & 题型总结 & 思路讲解

文章目录

  • 四、分割整数

    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

    ```

    参考:动态规划 LeetCode 题解

posted @ 2021-01-06 14:14  刘桓湚  阅读(155)  评论(0编辑  收藏  举报