对于大多数学习编程的人来说,动态规划一直是一个难突破的点,我自己在学习过程中也是很困惑,各种书籍资料,视频都看过不少,基本的思想也都能理解,但一碰到题目的时候还是觉得无从下手。最近在做笔试题的时候突然有了一丢丢理解,这里记录下, 没准路过看到的人能加深理解或有所启发。

逆向思维

题目如下:

一个人在 m×n 的方格里行走,每次只能向右或向下,每个方格里的值是距离,求从起点走到终点的最短路径值。
示例:3 3
1 3 1
1 5 1
4 2 1
最终输出 7 ,从左上走到右下所花费的最短距离是 7

第一眼看到题目的时候,就知道是动态规划类的题了,然而在做题时由于各(tai)种(cai)原(le)因还是没能做出来,尽管对这种类型的题目见得多了。有种我熟悉它它却不待见我的赶脚。等到结束之后回过头来想,想啊想啊想 … 半小时过去了,又想啊想,总算是把代码实现出来了。

按照正常思维来看的话,人站在起点处,向右或向下走到终点,那么最短距离就是每次比较向左或向下的路程,取最小的走,在加上自身需要的路程,这样一直走到终点就是最短的路径值。对照着题目验证下,那就是 1 -> (向下) 1 -> (向下) 4 -> (向右) 2 -> (向右) 1 到达终点,总距离是 9 ,发现这种走法是错的。正确的走法应该是 1 -> 3 -> 1 -> 1 -> 1 ,总距离是 7 最短。

image
image

此方法不行,那我们来考虑第二种解法。现在,不从起点来推算了,因为我们一眼看过去题目这个矩阵,就知道在这个输入下,最好的路径就是 1 -> 1 -> 1 -> 3 -> 1 这样走法,怎么得出来的呢?我们从矩阵最后一个数看,它是终点,那这个终点是如何过来的呢? 发现它是由左边或者上边这个数字过来的,为什么会这样?因为题目说了,只能向右或向下走,所以终点的这个位置要么是上面位置向下走得来的,要么就是左边位置向右走到达的。所以我们从后往前推得出 1 -> 1 -> 1 -> 3 -> 1 这种最佳走法。如下图:

image
image

这就是传说中的动态规划 , 在这道题里,我们定义的一个状态是 f(i, j) ,表示从起点走到坐标(i, j) 所经历的最短路径,由上面的推断可以发现,坐标 (i,j)的最短路径总是由 坐标(i-1, j) 和坐标 (i, j-1) 两个的最小值加上坐标 (i, j) 自身的值得来
状态转移方程为 f(i, j) = Math.min(f(i-1 ,j), f(i, j-1)) + values[i, j] i, j 为二维数组的坐标,values 为数组对应坐标的初始值。原始数组 values 和 dp 数组的推算值如下图:

image
image

有了上面的铺垫,我们就可以得出代码了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function solve(arr, m, n) {
//定义一个二维数组 dp , dp[i][j]表示走到i,j这个点的最短路径
var dp = new Array(m);
for (var i = 0; i < m; i++) {
dp[i] = [];
for (var j = 0; j < n; j++) {
dp[i][j] = 0;
}
}
// 初始化第一行和第一列的状态值
dp[0][0] = arr[0][0];
for (var i = 1; i < n; i++) {
dp[0][i] = arr[0][i] + dp[0][i - 1];
}
for (var i = 1; i < m; i++) {
dp[i][0] = arr[i][0] + dp[i - 1][0];
}
// 动态规划,计算每个点的最短路径值
for (var i = 1; i < m; i++) {
for (var j = 1; j < n; j++) {
dp[i][j] = arr[i][j] + Math.min(dp[i - 1][j], dp[i][j - 1]);
}
}
return dp[m - 1][n - 1];
}

我们用一个二维数组dp , 来存储每个节点的最短路径值,这里要特别注意的是,需要对数组的第一行和第一列进行初始化,第一行的每个点只能由左边向右走过来,所以直接累加相应坐标的值;同理第一列的每个点只能由上面向下走得来,每个点由原来的值加上一个点的值即可。接下来从第 1 行,第1列开始,一直遍历到 m -1, n -1 ,把每个状态值填上,函数的返回值为走到终点的坐标也就是 dp[m-1][n-1] 。

动态规划的思想

在学习动态规划的时候,学的一愣一愣的,什么最优子结构、无副作用、重复子问题等,看着这些理论似懂非懂。

什么是动态规划?我觉得动态规划就是逆向,由后往前地去推导这样的过程,就仿佛开了上帝视角一样,在已经知道结果的情况下,通过对各种情况的抉择,去倒推出原始的起点,然后定义一个合理的状态来解释。像上面的这道题目,问你一个人从左上角走到右下角的最短路程,那我们在看题的时候,可以全局地看到各个路径的距离值以及周围的路径分布,所以可以通过推算很快得出结果。上面的第一种解法其实就是贪心的思路,从起点开始,每次选择一条最短的路径走下去,这样带来的弊端就是可能后面的路径是更加糟糕的,容易因为短视而得不到最佳的结果。

总的来说,在遇到一些求最佳问题时,比如求最短路径,最大价值、最长公共子序列等问题时,就要考虑使用动态规划来求解,而有些时候,不妨先用贪心的思想来试试能否求得结果,这样也能明白不行的原因在哪里,也能加深对动态规划的应用场景理解。

后记

很多时候看懂了不一定真的懂了,正如开头提到的知道题目是动态规划,但就是觉得无从下手,现在看还是要多做多理解,也许过段时间又有新的领悟。

对这个知识点的理解还有待加深,文章有什么不对之处,敬请指正,欢迎交流探讨。

(完)