文章目录
1. 引言
在CH40中以0-1背包问题,从回溯算法到备忘录再到引入动态规划,可以从实践角度看到动态规划是如何解决问题的。
知其然,更要知其所以然,那么本节将站在更高的角度来看待动态规划能够解决什么类型的问题,从理论的角度来总结一下动态规划,为未来遇到更多更抽象的问题提供指导方向。
2. 动态规划可以解决什么问题
动态规划是一个非常成熟的算法思想,其解决问题的特征已经被很好的总结出来,一句话“一个模型,三个特征”。
2.1 一个模型
一个模型是“多阶段决策最优解模型”。
动态规划一般用来解决最优化问题,而解决问题往往分为多个决策阶段,每个阶段对应不同的状态。我们需要寻找一组决策序列,从而达成最优结果。
一般多阶段决策问题,可以通过回溯算法枚举出所有可能的决策序列,然后选出最优解。但是动态规划通过“合并重复状态,剔除无效状态”大大提高了算法效率,这是动态规划存在的重要意义。
2.2 三个特征
2.2.1 最优子结构
最优子结构是指:问题的最优解包含子问题的最优解,我们可以通过子问题的最优解推出原问题最优解。
对应到动态规划中,就是我们可以通过前面阶段状态推出后面阶段的状态。
2.2.2 无后效性
无后效性从两个方面解释。
- 后面阶段状态由前面阶段状态推出,而不关心前面阶段状态如何得。
- 前面阶段状态,不会受到后面阶段决策的影响。
实际上只要满足前面提到的一个模型,都会满足无后效性。
2.2.3 重复子问题
动态规划本质上是在回溯算法的基础上,合并重复状态、剔除无效状态。因此重复子问题表示不同的决策序列可能会产生重复的状态。
3. 动态规划常规解题思路
有了上边的理论基础,这里总结出两个解决动态规划问题的一般思路。
以一个问题为例,如下图,有一个 n 乘以 n 的矩阵,存储都是正整数。棋子起始位置在左上角,终止位置在右下角。每次可以将棋子向右或者向下移动一位,从左上角到右下角有很多不同的路径,把路径经过的数字加起来看作路径长度。求最短路径?
3.1 状态转移表法
- 画出一个状态表,注意状态表一般是二维的,包含三个变量(对比升级版的0-1背包问题),行、列、数值。一旦二维状态表无法表达所需状态,需要三维甚至更高维的状态表,则说明该问题不适合用状态表来分析。
- 画出状态表后,按照决策过程先后来递推填充表中的状态。
- 将填表过程翻译成代码,就完成了动态规划了。
针对最短路径问题,可以看成图的广度优先搜索,如下图,可以把问题分成6个阶段。
画出状态表,表中行列表示棋子所在位置,数值表示从起点到该位置的最短路径,填表过程,如下图。
填表过程完毕后,我们将其翻译成代码如下:
public int minDistDP(int[][] matrix, int n) {
int[][] states = new int[n][n];
int sum = 0;
for (int j = 0; j < n; ++j) { // 初始化states的第一行数据
sum += matrix[0][j];
states[0][j] = sum;
}
sum = 0;
for (int i = 0; i < n; ++i) { // 初始化states的第一列数据
sum += matrix[i][0];
states[i][0] = sum;
}
for (int i = 1; i < n; ++i) {
for (int j = 1; j < n; ++j) {
states[i][j] =
matrix[i][j] + Math.min(states[i][j-1], states[i-1][j]);
}
}
return states[n-1][n-1];
}
3.2 状态转移方程法
状态转移方程法有点类似递归的解题思路。我们需要分析,某个问题如何通过子问题来递归求解,也就是所谓的最优子结构,根据最优子结构,写出递归公式,也就是所谓的状态转移方程。
有了状态转移方程,代码实现就非常简单了,一般情况下,我们有两种代码实现方法,一种是递归加“备忘录”,另一种是迭代递推。
状态转移方程是解决动态规划的关键,写出状态转移方程,动态规划问题基本上就解决一大半了,而翻译成代码非常简单。但是很多动态规划问题的状态本身就不好定义,状态转移方程也就更不好想到。
对于上述问题,状态转移方程如下:
min_dist(i, j) = w[i][j] + min(min_dist(i, j-1), min_dist(i-1, j))
用递归加备忘录实现如下:
private int[][] matrix =
{{1,3,5,9}, {2,1,3,4},{5,2,6,7},{6,8,4,3}};
private int n = 4;
private int[][] mem = new int[4][4];
public int minDist(int i, int j) { // 调用minDist(n-1, n-1);
if (i == 0 && j == 0) return matrix[0][0];
if (mem[i][j] > 0) return mem[i][j];
int minLeft = Integer.MAX_VALUE;
if (j-1 >= 0) {
minLeft = minDist(i, j-1);
}
int minUp = Integer.MAX_VALUE;
if (i-1 >= 0) {
minUp = minDist(i-1, j);
}
int currMinDist = matrix[i][j] + Math.min(minLeft, minUp);
mem[i][j] = currMinDist;
return currMinDist;
}
用迭代递推,实现代码就和状态表法一致。