动态规划(Dynamic Programming, DP)是解决优化问题的一种算法策略,它将一个复杂问题分解为更小的子问题,通过解决子问题来逐步找到复杂问题的最优解。动态规划适用于有重叠子问题和最优子结构性质的问题。接下来,我们通过一个经典的动态规划问题——斐波那契数列(Fibonacci Sequence)来详细介绍动态规划的思路和实现步骤。
问题定义
斐波那契数列是这样一个序列:0, 1, 1, 2, 3, 5, 8, 13, 21, 34, …,其中每个数字(从第三个数字起)都是前两个数字的和。斐波那契数列的定义如下:
我们的目标是编写一个函数,输入n,输出斐波那契数列的第n项。
1. 递归解法(非DP解)
首先,我们尝试使用递归解法,这种方法简单直观,但效率较低。
def fibonacci_recursive(n):if n <= 1:return nelse:return fibonacci_recursive(n-1) + fibonacci_recursive(n-2)
这个递归解法虽然简单,但它进行了大量重复计算,时间复杂度为O(2^n)。
我们可以通过分析斐波那契数列的递归树来直观地看出重复计算的发生。
斐波那契数列的递归树
考虑计算F(5)
的过程,递归树如下所示:
在这个树状结构中,每个节点表示一个递归调用,节点的值是调用的参数。从这个树中我们可以观察到:
-
重复的子问题:
F(3)
、F(2)
、F(1)
和F(0)
被计算了多次。特别是F(2)
,在整个计算过程中被重复计算了三次。 -
增长的递归调用:随着参数
n
的增加,递归调用的数量呈指数级增长。例如,F(n)
的计算会导致F(n-1)
和F(n-2)
的计算,而这些调用又会分别导致更多的递归调用。
分析重复计算的原因
重复计算的主要原因在于,递归解法中对每个子问题的解决都是独立进行的,它没有考虑到在求解过程中,很多子问题实际上是相同的。由于递归方法没有记录已经解决的子问题的结果,每次遇到这些子问题时,它都会重新进行计算。
2. 动态规划解法
为了提高效率,我们使用动态规划的方法,避免重复计算。
步骤1:定义状态
定义dp数组,其中dp[i]表示斐波那契数列中第i个数的值。
步骤2:确定状态转移方程
根据斐波那契数列的定义,我们可以得到状态转移方程:dp[i] = dp[i-1] + dp[i-2]
步骤3:确定初始条件和边界条件
dp[0] = 0, dp[1] = 1
步骤4:计算顺序
从小到大计算dp数组的值。
动态规划代码实现
def fibonacci_dp(n):if n <= 1:return ndp = [0] * (n+1)dp[1] = 1for i in range(2, n+1):dp[i] = dp[i-1] + dp[i-2]return dp[n]
这种方法的时间复杂度为O(n),空间复杂度也为O(n)。
3. 动态规划优化
对于斐波那契数列问题,我们实际上不需要保存整个dp数组,只需要保存前两个状态即可。
优化后的代码
def fibonacci_dp_optimized(n):if n <= 1:return nprev, curr = 0, 1for i in range(2, n+1):prev, curr = curr, prev + currreturn curr
这个优化后的版本将空间复杂度降低到了O(1)。
4. 算法思考
1. 子问题
在动态规划中,我们将原问题分解为较小的、相互关联的子问题。对于斐波那契数列问题,求F(n)
可以看作是一个原问题,它可以分解为求F(n-1)
和F(n-2)
两个子问题。而F(n-1)
和F(n-2)
又可以继续分解,直到F(1)
和F(0)
。
2. 重叠子问题
动态规划适用于那些有大量重叠子问题的情况,即不同的问题求解路径中包含了许多相同的子问题。在递归求解斐波那契数列时,F(n-1)
和F(n-2)
会重复计算F(n-3)
、F(n-4)
等更小的子问题。动态规划通过记忆化(存储)这些子问题的解来避免重复计算。
3. 最优子结构
斐波那契数列问题具有最优子结构的特点,即问题的最优解包含了其子问题的最优解。虽然斐波那契数列问题本身不涉及“最优化”,但其解决方法体现了最优子结构的思想:F(n)
的最优解(即准确解)可以通过组合F(n-1)
和F(n-2)
的解得到。
4. 动态规划表(DP Table)
在这个例子中,dp
数组就是所谓的动态规划表。它用来记录每一步的结果(即每个F(i)
的值),以便于后续的计算可以直接引用前面的结果,而不是重新计算。这种方法极大地提高了效率,因为每个子问题只被解决一次,并且一旦被解决,其结果就会被保存。
5. 状态转移方程
动态规划的核心是状态转移方程。在斐波那契数列的例子中,状态转移方程是dp[i] = dp[i-1] + dp[i-2]
。这个方程描述了问题状态之间的关系,即如何从已知的子问题的解得到当前问题的解。
通过这个斐波那契数列的例子,你可以看到,尽管我们使用的是简单的数组来存储每一步的结果,但这个过程完全体现了动态规划的思想:分解问题、解决子问题、存储子问题的解以避免重复计算。这就是dp
数组与动态规划思想关联的方式,它是实现动态规划策略的一个工具。
总结
动态规划中的应用体现了动态规划的核心思想:存储中间结果,避免重复计算。这里的dp
数组正是用来存储每个子问题的解,即斐波那契数列中每个位置的数值。让我们更深入地理解它与动态规划思想的关联: