吃掉 N 个橘子的最少天数

标签: 记忆化搜索 动态规划

难度: Hard

厨房里总共有 n 个橘子,你决定每一天选择如下方式之一吃这些橘子:

  • 吃掉一个橘子。
  • 如果剩余橘子数 n 能被 2 整除,那么你可以吃掉 n/2 个橘子。
  • 如果剩余橘子数 n 能被 3 整除,那么你可以吃掉 2*(n/3) 个橘子。

每天你只能从以上 3 种方案中选择一种方案。

请你返回吃掉所有 n 个橘子的最少天数。

示例 1:

输入:n = 10
输出:4
解释:你总共有 10 个橘子。
第 1 天:吃 1 个橘子,剩余橘子数 10 - 1 = 9。
第 2 天:吃 6 个橘子,剩余橘子数 9 - 2*(9/3) = 9 - 6 = 3。(9 可以被 3 整除)
第 3 天:吃 2 个橘子,剩余橘子数 3 - 2*(3/3) = 3 - 2 = 1。
第 4 天:吃掉最后 1 个橘子,剩余橘子数 1 - 1 = 0。
你需要至少 4 天吃掉 10 个橘子。

示例 2:

输入:n = 6
输出:3
解释:你总共有 6 个橘子。
第 1 天:吃 3 个橘子,剩余橘子数 6 - 6/2 = 6 - 3 = 3。(6 可以被 2 整除)
第 2 天:吃 2 个橘子,剩余橘子数 3 - 2*(3/3) = 3 - 2 = 1。(3 可以被 3 整除)
第 3 天:吃掉剩余 1 个橘子,剩余橘子数 1 - 1 = 0。
你至少需要 3 天吃掉 6 个橘子。

示例 3:

输入:n = 1
输出:1

示例 4:

输入:n = 56
输出:6

提示:

  • 1 <= n <= 2*10^9

Submission

运行时间: 33 ms

内存: 18.1 MB

class Solution:
    @lru_cache(None)
    def minDays(self, n: int) -> int:
        if n <= 1:
            return n
        return min(n % 2 + 1 + self.minDays(n // 2), n % 3 + 1 + self.minDays(n // 3))

Explain

这是一个最优化问题,使用动态规划的思路来解决。首先,我们定义函数 `minDays(n)` 表示吃掉 `n` 个橘子的最小天数。如果 `n` 小于等于 1,直接返回 `n`,因为如果有一个橘子,需要一天吃掉;没有橘子则不需要任何天数。对于 `n > 1`,两种策略的选择:一是每次吃掉 `n//2` 个橘子,这需要 `n % 2 + 1` 天(`n % 2` 天处理不能被2整除的部分,1天处理剩余的部分),或者每次吃掉 `2*(n//3)` 个橘子,这需要 `n % 3 + 1` 天(同理,处理不能被3整除的部分)。使用递归和记忆化(通过 `lru_cache`)来存储已经计算过的结果,避免重复计算,从而提高效率。

时间复杂度: O(log(n))

空间复杂度: O(log(n))

class Solution:
    @lru_cache(None)
    def minDays(self, n: int) -> int:
        # 如果 n <= 1,直接返回 n(0 或 1 天)
        if n <= 1:
            return n
        # 选择吃一半橘子的方案 vs. 选择吃2/3橘子的方案
        # n % 2 + 1 为处理 n 不被 2 整除的情况需要的额外天数加上一天
        # n % 3 + 1 同理处理 n 不被 3 整除的情况
        return min(n % 2 + 1 + self.minDays(n // 2), n % 3 + 1 + self.minDays(n // 3))

Explore

在计算最小天数时,递归是一种自然和直观的方法来处理因数分解问题。对于n不被2或3整除的情况,我们的目标是将问题分解成更小的子问题,这些子问题可以通过相同的处理逻辑来解决。递归允许我们不断将问题规模缩小,直到达到基本情况(即n<=1)。此外,使用递归可以简化编码过程并保持程序的清晰性。虽然还可以考虑其他策略,如迭代或其他具体算法,但递归结合记忆化在处理此类最优化问题时常常是高效且易于实现的策略之一。

在定义`minDays(n)`函数时,每次递归调用都将n除以2或3,这意味着递归深度大约是log2(n)或log3(n),通常这是一个相对较小的值。因此,即使对于较大的n值,递归深度也不会非常深,不太可能导致栈溢出。然而,如果没有适当的优化,如记忆化或迭代替代,大量的递归调用可能会导致性能问题。使用`lru_cache`进行记忆化是避免重复计算和减少递归调用次数的有效方式,从而提高整体性能。

在当前的动态规划解法中,主要考虑了每天吃掉n的一半或三分之二的策略,这通常是减少橘子数量的快速方式。然而,这种方法并没有直接考虑连续多天只吃一个橘子的情况。理论上,连续多天吃一个橘子可能在某些具体情况下是有用的,但在大多数情况下,这种策略不会比直接减半或减去三分之二更优。不过,算法确实在递归过程中间接考虑了这种情况,因为递归的每一层都可能选择不同的策略,包括最终可能退化为每天吃一个橘子。

对于非常大的n值,递归解法即使有记忆化也可能面临性能问题,特别是在递归深度非常大时。在这种情况下,可以考虑使用迭代方法来优化算法。例如,可以使用迭代的动态规划表来自底向上计算解,避免递归带来的额外开销。迭代方法通常需要额外的空间来存储中间状态,但可以有效控制程序的执行流,避免深度递归可能导致的栈溢出问题。总之,根据具体问题的大小和资源限制,选择最合适的方法(递归或迭代)是关键。