返回

攀爬求知之巅:踏过楼梯,掘金无限

前端

在计算机科学的浩瀚世界中,有一种算法可以化繁为简,将复杂问题拆解成更易解决的子问题,从而逐步迈向最优解。这种算法,就是动态规划。

在本文中,我们将聚焦于两个经典的动态规划问题:爬楼梯问题和挖金矿问题。这两个问题看似简单,却蕴含着深刻的智慧与技巧。

一、踏上阶梯,攀登智慧高峰——爬楼梯问题

想象一下,你站在楼梯的底端,向上望去,无尽的阶梯延伸向天际。你的目标是到达楼梯的顶端,但你一次只能迈出一级或两级台阶。

如何才能以最少的步数到达顶端?这就是著名的爬楼梯问题。

1. 递归求解:一步一步,层层递进

对于这个看似简单的问题,一种常见的解决方法是递归。我们将楼梯分为一个个子问题,即从底端到第N级台阶的所有情况,其中N是楼梯的总阶数。

def climb_stairs(n):
  """
  递归求解爬楼梯问题

  Args:
    n: 楼梯的总阶数

  Returns:
    到达顶端所需的最小步数
  """

  if n == 1:
    return 1
  if n == 2:
    return 2

  return climb_stairs(n-1) + climb_stairs(n-2)

这种方法虽然简单直观,但存在一个明显的缺陷:大量的重复计算。例如,当n=5时,函数会分别计算从第1级台阶到第5级台阶的所有情况,而其中很多情况是重复的。

2. 动态规划:化繁为简,逐层递进

为了解决重复计算的问题,我们可以使用动态规划。动态规划是一种自底向上的方法,它将问题分解成更小的子问题,并逐层求解,存储子问题的解,避免重复计算。

def climb_stairs_dp(n):
  """
  动态规划求解爬楼梯问题

  Args:
    n: 楼梯的总阶数

  Returns:
    到达顶端所需的最小步数
  """

  dp = [0] * (n+1)
  dp[1] = 1
  dp[2] = 2

  for i in range(3, n+1):
    dp[i] = dp[i-1] + dp[i-2]

  return dp[n]

在动态规划的方法中,我们使用一个数组dp来存储子问题的解。dp[i]表示从底端到第i级台阶的所有情况中,到达顶端的最小步数。

我们从最简单的子问题开始,即从底端到第1级台阶和从底端到第2级台阶的情况。这两个子问题的解显然是1和2。

然后,我们逐层递进,计算每个子问题的解。在计算dp[i]时,我们可以利用dp[i-1]和dp[i-2]的值,因为从底端到第i级台阶的情况可以分解为从底端到第i-1级台阶和从第i-1级台阶到第i级台阶的情况。

这种方法大大减少了计算量,将时间复杂度从递归方法的指数级降低到了线性级。

二、开采财富,掘金无限——挖金矿问题

现在,让我们把目光转向另一个经典的动态规划问题:挖金矿问题。

想象一下,你是一个矿工,你拥有一块金矿。金矿被分成了一块块金矿格,每个金矿格都包含一定数量的金子。

你的目标是开采金矿,获得最多的金子。但是,你只能从左上角的金矿格开始开采,并且只能向下或向右移动。

如何才能开采出最多的金子?这就是著名的挖金矿问题。

1. 递归求解:逐格探索,层层深入

和爬楼梯问题一样,我们可以使用递归来解决挖金矿问题。我们将金矿分成一个个子问题,即从左上角的金矿格到第i行第j列的所有金矿格的情况。

def dig_gold(grid, i, j):
  """
  递归求解挖金矿问题

  Args:
    grid: 金矿格的二维数组
    i: 当前所在的金矿格的行号
    j: 当前所在的金矿格的列号

  Returns:
    从左上角的金矿格到当前金矿格所能获得的最大金子数量
  """

  if i == len(grid) or j == len(grid[0]):
    return 0

  return max(
    dig_gold(grid, i+1, j),  # 向下移动
    dig_gold(grid, i, j+1)   # 向右移动
  ) + grid[i][j]

这种方法虽然简单直观,但同样存在大量的重复计算。

2. 动态规划:步步为营,逐格递进

为了解决重复计算的问题,我们可以使用动态规划。我们将使用一个二维数组dp来存储子问题的解。dp[i][j]表示从左上角的金矿格到第i行第j列的所有金矿格的情况中,所能获得的最大金子数量。

def dig_gold_dp(grid):
  """
  动态规划求解挖金矿问题

  Args:
    grid: 金矿格的二维数组

  Returns:
    从左上角的金矿格到右下角的金矿格所能获得的最大金子数量
  """

  m = len(grid)
  n = len(grid[0])
  dp = [[0 for _ in range(n)] for _ in range(m)]

  dp[0][0] = grid[0][0]

  for i in range(1, m):
    dp[i][0] = dp[i-1][0] + grid[i][0]

  for j in range(1, n):
    dp[0][j] = dp[0][j-1] + grid[0][j]

  for i in range(1, m):
    for j in range(1, n):
      dp[i][j] = max(
        dp[i-1][j],  # 向下移动
        dp[i][j-1]   # 向右移动
      ) + grid[i][j]

  return dp[m-1][n-1]

在动态规划的方法中,我们从最简单的子问题开始,即从左上角的金矿格到第1行第1列的所有金矿格的情况。这个子问题的解显然是grid[0][0]。

然后,我们逐行逐列地计算每个子问题的解。在计算dp[i][j]时,我们可以利用dp[i-1][j]和dp[i][j-1]的值,因为从左上角的金矿格到第i行第j列的所有金矿格的情况可以分解为从左上角的金矿格到第i-1行第j列的所有金矿格和从左上角的金矿格到第i行第j-1列的所有金矿格的情况。

这种方法大大减少了计算量,将时间复杂度从递归方法的指数级降低到了线性级。

结语

通过爬楼梯问题和挖金矿问题,我们领略了动态规划的强大威力。动态规划可以将复杂问题分解成更易解决的子问题,并逐层求解,最终得到最优解。

动态规划是一种非常重要的算法技巧,它被广泛应用于计算机科学的各个领域,例如运筹学、机器学习和人工智能等。

希望本文能够帮助您更好地理解动态规划的思想和技巧,并将其应用到您的实际工作和学习中。