什么是动态规划?
动态规划(Dynamic Programming, DP)是一种解决复杂问题的方法,通过将问题分解为更简单的子问题来解决。动态规划通常用于优化问题,特别是当一个问题可以被分解为多个重叠的子问题时。其核心思想是通过保存子问题的结果(记忆化)来避免重复计算,从而提高效率。
动态规划的基本步骤
- 定义子问题:将原问题分解为若干子问题。
- 猜测(猜解法):根据子问题的解,猜测原问题的解。
- 递归和记忆化:通过递归求解子问题,并使用记忆化技术保存子问题的解。
- 递推关系:根据子问题的解,找到原问题解的递推关系。
- 解决原问题:通过递推关系,逐步求解原问题。
经典例子:斐波那契数列
我们先从一个简单的例子开始:斐波那契数列。
斐波那契数列的定义
斐波那契数列是一个经典的递归问题,定义如下:
F
(
n
)
=
F
(
n
−
1
)
+
F
(
n
−
2
)
F(n) = F(n-1) + F(n-2)
F(n)=F(n−1)+F(n−2)
F
(
0
)
=
0
,
F
(
1
)
=
1
F(0) = 0, F(1) = 1
F(0)=0,F(1)=1
动态规划解法
我们可以使用动态规划来避免重复计算。
package main
import "fmt"
// 计算第n个斐波那契数
func fibonacci(n int) int {
if n <= 1 {
return n
}
// 创建一个数组来存储斐波那契数
dp := make([]int, n+1)
// 初始化base cases
dp[0] = 0
dp[1] = 1
// 自底向上计算斐波那契数
for i := 2; i <= n; i++ {
dp[i] = dp[i-1] + dp[i-2]
}
// 返回结果
return dp[n]
}
func main() {
n := 10
fmt.Printf("第%d个斐波那契数是: %d\n", n, fibonacci(n))
}
解释
-
初始化:
dp := make([]int, n+1) dp[0] = 0 dp[1] = 1
创建一个数组
dp
来存储斐波那契数,从F(0)
到F(n)
。 -
填表:
for i := 2; i <= n; i++ { dp[i] = dp[i-1] + dp[i-2] }
使用循环自底向上计算每个斐波那契数,避免了重复计算。
-
返回结果:
return dp[n]
最后返回
F(n)
。
背包问题
我们再来看一个稍微复杂的例子:01背包问题。
01背包问题的描述
01背包问题是一个经典的动态规划问题,描述如下:
- 有
n
个物品,每个物品都有一个重量weights[i]
和一个价值values[i]
。 - 给定一个容量为
W
的背包,选择物品放入背包使得总重量不超过W
,并且背包中的总价值最大。
动态规划的解法
动态规划的核心思想是将问题分解为子问题,并通过保存子问题的结果来避免重复计算。
具体来说,我们通过构建一个二维数组dp
来记录子问题的解,其中dp[i][w]
表示前i
个物品中,总重量不超过w
的最大价值。
转移方程
根据当前物品是否放入背包,可以分两种情况:
- 不放入第
i
个物品:那么最大价值等于前i-1
个物品在总重量w
时的最大价值,即dp[i][w] = dp[i-1][w]
。 - 放入第
i
个物品:那么最大价值等于前i-1
个物品在总重量w - weights[i-1]
时的最大价值加上第i
个物品的价值,即dp[i][w] = dp[i-1][w-weights[i-1]] + values[i-1]
。
因此,转移方程为:
d
p
[
i
]
[
w
]
=
max
(
d
p
[
i
−
1
]
[
w
]
,
d
p
[
i
−
1
]
[
w
−
w
e
i
g
h
t
s
[
i
−
1
]
]
+
v
a
l
u
e
s
[
i
−
1
]
)
dp[i][w] = \max(dp[i-1][w], dp[i-1][w-weights[i-1]] + values[i-1])
dp[i][w]=max(dp[i−1][w],dp[i−1][w−weights[i−1]]+values[i−1])
代码详细解释
package main
import "fmt"
// 01背包问题
func knapsack(weights, values []int, W int) int {
n := len(weights)
// 创建二维数组dp,初始化为0
dp := make([][]int, n+1)
for i := range dp {
dp[i] = make([]int, W+1)
}
// 填表,i从1到n表示物品,w从0到W表示当前背包容量
for i := 1; i <= n; i++ {
for w := 0; w <= W; w++ {
if weights[i-1] <= w {
// 取不放入和放入物品的最大值
dp[i][w] = max(dp[i-1][w], dp[i-1][w-weights[i-1]] + values[i-1])
} else {
// 当前物品不能放入背包,继承前一状态
dp[i][w] = dp[i-1][w]
}
}
}
// 返回最大价值
return dp[n][W]
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
func main() {
weights := []int{2, 1, 3, 2}
values := []int{3, 2, 4, 2}
W := 5
fmt.Printf("最大价值是: %d\n", knapsack(weights, values, W))
}
步骤解释
-
初始化二维数组
dp
:dp := make([][]int, n+1) for i := range dp { dp[i] = make([]int, W+1) }
创建一个大小为
(n+1) x (W+1)
的二维数组dp
,初始化为0。dp[i][w]
表示前i
个物品中,总重量不超过w
的最大价值。 -
填表过程:
for i := 1; i <= n; i++ { for w := 0; w <= W; w++ { if weights[i-1] <= w { dp[i][w] = max(dp[i-1][w], dp[i-1][w-weights[i-1]] + values[i-1]) } else { dp[i][w] = dp[i-1][w] } } }
外层循环遍历每个物品
i
,内层循环遍历每个可能的背包容量w
。- 如果当前物品的重量小于等于当前容量
w
,则考虑两种情况:不放入当前物品或放入当前物品,取两者的最大值。 - 如果当前物品的重量大于当前容量
w
,则不能放入当前物品,直接继承前一个状态的最大值。
- 如果当前物品的重量小于等于当前容量
-
返回结果:
return dp[n][W]
返回
dp[n][W]
,即考虑所有物品,在背包容量为W
时的最大价值。
通俗解释
假设我们有一个背包,容量是5
,现在有四个物品,每个物品都有一个重量和一个价值。我们要做的就是决定哪些物品放入背包,使得背包中的总价值最大,同时总重量不能超过5
。
我们通过动态规划的方法,逐个考虑每个物品和每种可能的背包容量,记录下在每种情况下的最大价值。最后,我们就可以得到在背包容量为5
时,能够取得的最大价值。
这个过程类似于在一个表格中填数字,通过比较每一步可能的结果,逐步找到最优解。
总结
动态规划通过将问题分解为子问题,并保存子问题的结果来提高效率。通过定义状态和转移方程,我们可以逐步求解复杂问题。