终结DP(动态规划)之路(一)

277 阅读6分钟

本文是动态规划的第一篇文章,主要解释DP分类中的第一个问题:Minimum (Maximum) Path to Reach a Target

在文章内容上以一个完全没有接触过DP的角度来写,保证一定能够看懂在说什么,主要对LeetCode的两道题做分析,题目如下:

746.题目介绍

  1. 可以从index 0或者1 开始
  2. 花费cost[i], 可以jump 1 or 2 steps
  3. Choose 1 or 2 steps depends on cost of that steps

做题的关键在于对题目的抽象建模能力,作为刚接触DP的新手,想当然的就从给的example里总结规律,但example有限,所以总结的规律也会有限。一开始我是这么想的:

Input: cost = [1, 100, 1, 1, 1, 100, 1, 1, 100, 1]

1.从index 0 or 1开始取决于cost[0] or cost[1] 小。

2.然后继续jump 1 or 2步取决于cost[i+1] or cost[i+2] 小

3.如果cost[i+1] == cost[i+2],取2步(因为这样更快到达upstairs)

但是,但是!

根据下面这个case,上面的部分规律就不work了。

Input: cost = [10, 15, 20]

根据规则1:应该取index 0,根据规则2,取index 1 总的cost=10+15=25

但其实应该取index 1,直接到upstairs,总的cost=15

根据case2,直观的感受是:从index 0 or 1开始还受限于是否能“一步登天”,如果一步登顶的cost < 2步登顶的cost,要选择一步登顶的index,要看全局最优,而不是index 0 or 1的cost大小。

这么分析下来就很尴尬了,因为case2很难用公式去描述,从而造成无从下手,放弃ing.

上述的思路,完全是一个new graduate 基于人类的常识下意识的分析问题的方式;既然这种思路不好解决问题,那我们换一种思路。

之前看过一篇文章讲DP,通俗易懂,是关于一个”国王和金矿的故事“,链接:www.cnblogs.com/sdjl/articl…

故事中最重要的点之一是backtrace的方式去解决问题(其他的点可以看文章,类似记录中间值,减少重复计算之类的,不是本文重点),不是从小的问题开始,而是做出假设,从最大的问题开始(假设挖第9座金矿,和不挖第9座金矿两者对比)。

所以这里,我们也从最终的问题开始,逐步去分解,用backtrace的方法去梳理思路试试。

首先我们需要对问题做抽象,题目中给的cost是每个stair的cost,我们需要的是在到这个stair为止,最少的cost是多少。所以首先要定义出来第i步min cost:mc[],这样我们才能采用backtrace的方式去说:”我想知道index i 的最小cost,即mc[i],只需要知道mc[i-1],和mc[i-2]谁最小,再加上cost[i]就能知道mc[i]了“

所以,对于该题,重点在于能够抽象出来mc[]的定义,这个至关重要,甚至我认为比推导出来递推公式还重要,因为它打通了从实际问题到建模的中间步骤。

第二点是递推公式:mc[i] = Math.min(mc[i-1],mc[i-2]) + cost[i];

最后一点细节需要处理的问题是:针对于cost[]数组,我要跳到顶楼是要到cost.length的index,举例:

cost = [10, 15, 20]

我要调到index = 3才是问题的终点,而不是index 2;那么我就可以从index 1跳上来,也可以从index 2跳上来。

解决了这两个问题,该题也就做出来了。

class Solution {
    public int minCostClimbingStairs(int[] cost) {
        int n = cost.length;
        int[] mc = new int[n+1];
        mc[0] = cost[0];
        mc[1] = cost[1];
        for (int i=2;i<=n;i++) {
            mc[i] = Math.min(mc[i-1], mc[i-2]) + (i == n?0:cost[i]);
        }
        return mc[n];
    }
}

64.题目介绍

同类型的第二题:既然说是同类型,那么我们就先找到类似的点,在上一题中,我们其实总结了2点对该类型的题的解题关键,再重申一下就是:

  1. 要求最后一个台阶的最小cost,必须抽象出来到某级台阶的最小cost这个概念(是数组的形式)
  2. 然后写出来递推公式:mc[i] = Math.min(mc[i-1],mc[i-2]) + cost[i]

所以,在本题中,我们也来看看是否有和上一题一样的特点:

上一题 本题
1 台阶可以一次走1步,也可以一次走2步 图表每次可以向下走,也可以向右走
2 本级台阶的min cost取决于上一步和上两步的值 当前方格的min sum取决于左侧和上侧的值

可以明显的看出来是一样的特征,只不过表现形式不一样,上题是一维的问题,所以用一维数组就可以表示。本题是二维的问题(下,右两个方向),所以用二维数组表示,仅此区别。

所以套用上题的解决思路,主要是解决好两个问题就可以了:

  1. 抽象概念:到该方格的min sum是多少,既然是图表(二维),所以需要用二维数组来表示:mps[i][j]
  2. 找到递推公式,当前方格的min sum取决于左侧和上侧的值,所以用公式表示就是:mps[i][j] = Math.min(mps[i-1][j], mps[i][j-1]) + grid[i][j];

看递推公式是不是和上题很像啊?只不过用二维数组表示罢了... ...

最后一个问题:还记得在上题中要处理的最后一点细节吗?跳到顶楼的index 是 cost.length 而不是 cost.length-1,这个是上题的特征,需要特别处理;在本题中也有属于本题的特征,就是:

  1. 针对图表的第一行,影响它的值只有它左边相邻的值(很好理解啊,因为是第一行啊,上面没有元素了)
  2. 针对图表的第一列,影响它的值只有它上边相邻的值(同样很好理解啊,因为是第一列啊,左边没有元素了)

处理好这两个点,这题就做完了。

class Solution {
    public int minPathSum(int[][] grid) {
        int m = grid.length;
        int n = grid[0].length;
        int[][] mps = new int[m][n];
        for (int i=0;i<m;i++) {
            for (int j=0;j<n;j++) {
                if (i-1<0 && j-1<0) {
                    mps[i][j] = grid[i][j];
                }
                else if (i-1<0) {
                    mps[i][j] = mps[i][j-1] + grid[i][j];
                }
                else if (j-1<0) {
                    mps[i][j] = mps[i-1][j] + grid[i][j];
                }
                else {
                    mps[i][j] = Math.min(mps[i-1][j], mps[i][j-1]) + grid[i][j];
                }
                
            }
        }
        return mps[m-1][n-1];
    }
}