1 动态规划题目的特征
动态规划问题一定具备以下三个特征:
重叠子问题:在穷举的过程中(比如通过递归),存在重复计算的现象;
无后效性:子问题之间的依赖是单向性的,某阶段状态一旦确定,就不受后续决策的影响;
最优子结构:子问题之间必须相互独立,或者说后续的计算可以通过前面的状态推导出来。
2 写出状态转移方程
动态规划问题的核心是写出正确的状态转移方程,为了写出它,我们要先确定以下几点:
初始化状态:由于动态规划是根据已经计算好的子问题推广到更大问题上去的,因此我们需要一个“原点”作为计算的开端;
状态:找出子问题与原问题之间会发生变化的变量,这个变量就是状态转移方程中的参数;
决策:改变状态,让状态不断逼近初始化状态的操作。这个决策,就是状态转移方程状态转移的方向。
3 编码
- 定义状态转移参数
- 设计备忘录
- 实现状态转移方程
4 举例: 0-1背包问题
给你一个可放总重量为 W 的背包和 N 个物品,对每个物品,有重量 w 和价值 v 两个属性,那么第 i 个物品的重量为 w[i],价值为 v[i]。现在让你用这个背包装物品,问最多能装的价值是多少?
状态转移方程
递归的写法
/*
* tn: traversed n,即已经遍历过的物品;
* rw: reserved w,即背包还能容量的重量。
*/
DP(int tn, int rw) {
// 当遍历完所有物品时,就该返回 0 了,因为没有物品也就没有价值了
if tn < 0
return 0
// 当背包还能容纳的重量已经小于当前物品的重量时,显然这个物品不能放入背包
if rw < w[tn]
return DP(tn - 1, rw)
// 作出决策,该不该放入物品:
// 1. 放入:那么价值是 DP(tn - 1, rw - w[tn]);
// 2. 不放入:那么价值是 DP(tn - 1, rw)。
return max(DP(tn - 1, rw), DP(tn - 1, rw - w[tn]) + v[tn])
}
动态规划的写法
状态转移参数: 变化的重量 , 变化的容量
于是使用双层循环计算子问题, 通过决策从子问题中求出最值的答案
0-1背包问题用max函数来决策, 动态规划问题的核心就是找出决策函数
int dp(int[] w, int[] v, int N, int W) {
// 创建备忘录
int[][] dp = new int[N+1][W+1];
// 初始化状态
for (int i = 0; i < N + 1; i++) { dp[i][0] = 0; }
for (int j = 0; j < W + 1; j++) { dp[0][j] = 0; }
for (int tn = 1; tn < N + 1; tn++) { // 遍历每一件物品
for (int rw = 1; rw < W + 1; rw++) { // 背包容量有多大就还要计算多少次
if (rw < w[tn]) {
// 当背包容量小于第tn件物品重量时,只能放入前tn-1件
dp[tn][rw] = dp[tn-1][rw];
} else {
// 当背包容量还大于第tn件物品重量时,进一步作出决策
dp[tn][rw] = Math.max(dp[tn-1][rw], dp[tn-1][rw-w[tn]] + v[tn]);
}
}
}
return dp[N][W];
}
int solveDP() {
int N = 3, W = 5; // 物品的总数,背包能容纳的总重量
int[] w = {0, 3, 2, 1}; // 物品的重量
int[] v = {0, 5, 2, 3}; // 物品的价值
return dp(w, v, N, W); // 输出答案
}