返回

思维交锋 | 动态规划解丑数 II:庖丁解牛式拆解

后端

丑数 II:庖丁解牛式拆解

踏上算法修行之路,终将遇到难题。而丑数 II 就是一道考验你算法思维的试金石。它要求你找出第 n 个丑数。丑数即只包含质因数 2、3 和/或 5 的正整数。

面对这样一个看起来错综复杂的题目,我们可以借鉴庖丁解牛的智慧,将问题拆解成更易解决的子问题。动态规划正是这种拆解思想的完美实践。

动态规划:化繁为简的艺术

动态规划是一种解决问题的策略,其核心思想是将大问题拆解成一系列更易解决的子问题,然后从子问题入手,逐个解决,最终组合得到大问题的解。

我们把每一个子问题的解存储起来,以便当遇到相同的子问题时,可以快速地检索到解,避免重复计算。

动态规划具有两个核心特征:子问题最优性和重复子问题。子问题最优性意味着子问题的解可以用来得到原问题的最优解。而重复子问题是指子问题在求解过程中被多次遇到。

丑数 II 中的动态规划

对于丑数 II,我们可以定义子问题为:求第 n 个丑数。而动态规划的难点在于找到恰当的子问题。

丑数的性质为:每一个丑数都是由一个更小的丑数乘以 2、3 或 5 得到的。因此,我们可以将求解第 n 个丑数的问题分解成三个子问题:

  1. 第 n 个丑数是第 n-1 个丑数乘以 2 得到的。
  2. 第 n 个丑数是第 n-1 个丑数乘以 3 得到的。
  3. 第 n 个丑数是第 n-1 个丑数乘以 5 得到的。

我们只需要比较这三个子问题的结果,就能得到第 n 个丑数。

实现步骤

  1. 创建一个数组 dp 来存储丑数,dp[i] 表示第 i 个丑数。
  2. 初始化 dp[0] = 1,因为 1 是第一个丑数。
  3. 使用三个指针 p2、p3、p5 分别指向 dp 数组中的位置,这些位置分别存储着乘以 2、3 和 5 后的最小丑数。
  4. 对于 i 从 1 开始循环到 n,执行以下步骤:
    • 计算三个子问题的解:dp[i] = dp[p2] * 2、dp[i] = dp[p3] * 3 和 dp[i] = dp[p5] * 5。
    • 找到三个子问题的最小解,并将其存储在 dp[i] 中。
    • 更新指针 p2、p3 和 p5,使其指向 dp 数组中分别存储着乘以 2、3 和 5 后的最小丑数的位置。

代码实现

def nthUglyNumber(n):
    """
    :type n: int
    :rtype: int
    """
    if n <= 0:
        return 0

    # 创建一个数组 dp 来存储丑数
    dp = [0] * n

    # 初始化 dp[0] = 1
    dp[0] = 1

    # 创建三个指针 p2、p3、p5 分别指向 dp 数组中的位置
    p2 = 0
    p3 = 0
    p5 = 0

    # 循环从 1 开始到 n
    for i in range(1, n):
        # 计算三个子问题的解
        num2 = dp[p2] * 2
        num3 = dp[p3] * 3
        num5 = dp[p5] * 5

        # 找到三个子问题的最小解
        dp[i] = min(num2, num3, num5)

        # 更新指针 p2、p3 和 p5
        if dp[i] == num2:
            p2 += 1
        if dp[i] == num3:
            p3 += 1
        if dp[i] == num5:
            p5 += 1

    # 返回第 n 个丑数
    return dp[n-1]

结语

通过动态规划,我们成功地将丑数 II 问题分解成更易解决的子问题,并最终得到了问题的解。这种自底向上的思维方式,正是动态规划的魅力所在。

动态规划是一种强大的算法,可以用来解决很多复杂的问题。希望这篇文章能为你打开动态规划的大门,让你在算法的海洋中乘风破浪!