选择>行动>思考,好像是个死循环 -song。
动态规划(Dynamic Programming,简称DP)是一种解决问题的数学优化方法,通常用于解决具有重叠子问题和最优子结构性质的问题。它的基本思想是将问题拆分成小的子问题,先求解并保存子问题的解,然后通过这些子问题的解来求解原始问题,避免重复计算,从而提高效率。
最常见的动态规划问题包括最长公共子序列、最短路径、背包问题等。让我们通过一个简单的例子来理解动态规划:
例子:斐波那契数列
斐波那契数列是一个经典的动态规划问题。数列的定义是:F(0) = 0,F(1) = 1,F(n) = F(n-1) + F(n-2)(n ≥ 2)。
现在我们想要计算第 n 个斐波那契数,如果直接按照递归定义来计算,会存在大量的重复计算,效率很低。而动态规划的思想是从底向上逐步计算,并保存中间结果。
def fibonacci(n):
if n <= 1:
return n
# 初始化一个数组来保存中间结果
dp = [0] * (n + 1)
dp[1] = 1
# 从底向上逐步计算
for i in range(2, n + 1):
dp[i] = dp[i - 1] + dp[i - 2]
return dp[n]
# 测试
print(fibonacci(5)) # 输出 5
在这个例子中,通过动态规划的方式,我们避免了重复计算,将问题拆解成小的子问题,从而高效地计算出了斐波那契数列的第 n 项。这是动态规划的基本思想,通过保存中间结果,避免重复计算,提高算法效率。
这里可能还有很多朋友不懂,咱们继续通俗一点的说:
当我们解决问题时,有时候问题很大,我们会把它分成一些小问题,解决小问题的过程中,我们会保存一些信息,以便后面解决更大的问题。动态规划就是这样一种策略。
比如,我们想算斐波那契数列的第 n 项,斐波那契数列的规律是:第一个数字是0,第二个数字是1,后面的数字是前两个数字的和。数列的前几项是 0, 1, 1, 2, 3, 5, 8, 13, ... 。
现在,我们要算第 n 项,如果直接算可能会很慢,因为要重复计算很多次相同的数字。动态规划就是为了避免这种重复计算。
我们可以这样做:
- 从小问题开始,先算出前两个数字(0和1)。
- 把这两个数字存下来。
- 然后,计算第三个数字,它是前两个数字的和。存下来。
- 依次类推,计算第四个、第五个,一直到第 n 个数字。
在这个过程中,我们不是每次都从头算,而是用之前算过的结果,这样就避免了大量的重复计算。
简而言之,动态规划就是将一个大问题拆成一系列小问题,逐步解决这些小问题,并且把中间结果保存下来,避免重复计算,最终得到整个问题的解。
让我们用一个更简单的例子来解释动态规划:
问题:爬楼梯
假设你要爬一个楼梯,每次你只能迈一步或两步。问:爬到楼梯顶端有多少种不同的方式?
现在,我们可以使用动态规划来解决这个问题。
-
定义子问题: 要爬到第 n 级楼梯,我们可以从第 n-1 级走一步,或者从第 n-2 级走两步。
-
找出状态转移方程: 令
dp[i]
表示爬到第 i 级楼梯的不同方式数,那么dp[i] = dp[i-1] + dp[i-2]
。 -
初始化: 对于前两级楼梯,有
dp[0] = 1
(不需要爬),dp[1] = 1
(一种方式,爬一步)。 -
递推: 从第三级楼梯开始,使用状态转移方程逐步计算
dp[2]
、dp[3]
,一直到dp[n]
。 -
返回结果: 最终结果是
dp[n]
,表示爬到楼梯顶端的不同方式数。
看看这个通俗易懂的 Python 代码:
def climbStairs(n):
# 初始化
dp = [0] * (n + 1)
dp[0] = 1
dp[1] = 1
# 递推
for i in range(2, n + 1):
dp[i] = dp[i - 1] + dp[i - 2]
# 返回结果
return dp[n]
# 测试
result = climbStairs(4)
print(result) # 输出 5
这里的 dp[4]
就表示爬到第四级楼梯的不同方式数,结果是5。这个例子简单明了,但也展示了动态规划的基本思想:将大问题拆解成小问题,通过保存中间结果避免重复计算,从而高效解决问题。
再讲一个例子:
想象你站在一个楼梯底部,目标是爬到楼梯顶端。你每次可以迈一步或者迈两步。问题是:爬到楼梯顶端,有多少不同的方式?
解释:
-
第一步: 如果楼梯只有一级,你只有一种方式,就是迈一步就到了。
-
第二步: 如果楼梯有两级,你可以选择迈一步两次或者一次迈两步。所以,有两种方式。
-
第三步: 如果楼梯有三级,你可以从第一级迈两步,也可以从第二级迈一步。这样就把问题拆分成了前两级楼梯的问题。
-
第四步: 如果楼梯有四级,你可以从第三级迈一步,也可以从第二级迈两步。同样,这又把问题拆分成了前两级楼梯的问题。
这个过程可以一直持续下去。而这就是动态规划的思想:
-
拆分问题: 将大问题拆分成小问题,先解决小问题的答案。
-
保存中间结果: 为了避免反复计算,我们保存每一步的答案。
通过这种方式,你可以从楼梯底部一步一步地找到爬到楼梯顶端的不同方式数。
在代码中,我们用一个列表(dp
)来保存每一级楼梯的不同方式数。通过迭代计算,最终得到了爬到楼梯顶端的答案。
问题:买糖果
假设你去糖果店,你有一些零钱,而糖果有不同的价格。现在,你想知道用你的零钱有多少种方式可以买到糖果。
-
定义小问题: 想象你有一些零钱,要买到一块钱的糖果,有几种方式?要买到两块钱的糖果,有几种方式?
-
找出规律: 如果你知道了买到一块钱和两块钱的方式,你就能推导出买到三块钱的方式。因为可以从买到两块钱的情况下再加一个硬币,或者从买到一块钱的情况下再加两个硬币。
-
定义状态转移方程: 令
dp[i]
表示用你的零钱买到 i 块钱糖果的不同方式数。那么dp[i] = dp[i-1] + dp[i-2] + ...
。 -
初始化: 对于买到一块钱的糖果,有
dp[1] = 1
种方式(就是用一块钱买一个)。对于买到两块钱的糖果,有dp[2] = 2
种方式(可以用两个一块的或一个两块的)。 -
递推: 从买到三块钱的情况开始,一直递推到你想知道的金额。
-
返回结果: 最终结果是
dp[你的零钱]
,表示用你的零钱能够购买到糖果的不同方式数。
这个问题和前面爬楼梯的问题类似,只是现在我们在考虑的是零钱和糖果的价钱。