美文网首页JavaScript 进阶营
由一道笔试题看动态规划

由一道笔试题看动态规划

作者: 一个学前端的码农 | 来源:发表于2019-10-17 20:22 被阅读0次

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

    逆向思维

    题目如下:

    一个人在 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

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

    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

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

    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] 。

    动态规划的思想

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

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

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

    后记

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

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

    (完)

    相关文章

      网友评论

        本文标题:由一道笔试题看动态规划

        本文链接:https://www.haomeiwen.com/subject/mtlpmctx.html