动态规划入门级教程

动态规划入门级教程

学完本文后,你可以解决[509. 斐波那契数 - 力扣(LeetCode)], [70. 爬楼梯 - 力扣(LeetCode)], [746. 使用最小花费爬楼梯 - 力扣(LeetCode)], [62. 不同路径 - 力扣(LeetCode)], [63. 不同路径 II - 力扣(LeetCode)]这些适合初学者的dp问题。

理论基础:

首先要明确为什么存在DP算法? 我拿斐波那契数列举例, 很多同学认为这个题递归就很轻松地解决了(代码过于简单就不写了), 但是实际上你计算到40层的深度,你的时间复杂度上界是 2 40 2^{40} 240, 一般的编译器是无法有这样的深度的。所以我们有了DP算法,DP的核心思想在于利用过去的数据解决现在的问题。回到刚才斐波那契数列,其实我们在向下递归的时候,存在大量的重复计算,如果我们进行相应的剪枝,便可大大提高我们的效率.

for (int i = 0; i <= n; i++)
{
    dp[i] = dp[i-1] + dp[i-2];
}

一个循环内就可以计算结果。

大家可能觉得斐波那契数列这个题不需要DP就很自然的写出来了,其实这这道题很培养我们的DP逻辑。因为任何斐波那契数都只依赖于前一个和前二个数字,第3个数字等与第1个数字+第二个数字,以此类推,我们就完成了DP问题中的重要一步,递推公式。

DP[i]代表着??

我们接下来谈第二个重要事项,很多题解都写的含混不清,说只要你会递推公式即可解决DP问题,然后你跟着抄题解莫名其妙也过了,但是下次来了换一下条件又过不了了。这是因为你没有搞清楚DP最根本的症结,也就是DP[i]的含义。我们在斐波那契数列都知道i不就是第i个斐波那契数吗,这有什么含义,但是假如我换道题,[70. 爬楼梯 - 力扣(LeetCode)], 这里的dp[i]你能讲出来吗?很多同学就懵了到这。 这里的i代表着到i层楼梯有几种不同的方法,我们还是举例子,一次可以爬1或者2阶,初始位置是0,dp[1]是不是就是到1层有几种方法啊? 很明显只有一种。 那么dp[2]呢? 一种是一层一层爬,一种是直接爬两层, 所以是2。 问题来了, dp[3]是多少? 我们这样想,为了到达dp[3],我们必须到达dp[2]或者dp[1], dp[2]我们就爬一层, dp[1]就爬两层,这就是上文说的利用过去的数据解决现在的问题。所以DP[3]是dp[2] + dp[1]。

只有明确了dp的含义,你才能开始动手写出代码逻辑,所以我认为这才是dp问题中最重要的一步。给出上文题目答案。

 vector<int> dp;
    dp.resize(n + 1);
    if (n == 1)
        return 1;
    if (n == 2)
        return 2;
    dp[1] = 1;
    dp[2] = 2;
    for (int i = 3; i <= n; i++)
    {
        dp[i] = dp[i - 1] + dp[i -2];
    }

    return dp[n];

$$

$$

从哪里开始???

有了上面两个准备工作,我下面要谈的是下一步,也就是我反复强调的,过去的数据解决现在的问题。那么问题来了,过去的数据在哪里啊???在斐波那契数我们知道过去的dp[0] dp[1] 都是1, 爬楼梯我们知道了dp[1]和dp[2]。 那么问题又来了, 所有的DP问题,过去的数都是一致的吗???答案显然否定。来看这道题:[746. 使用最小花费爬楼梯 - 力扣(LeetCode)], 好多同学又懵了,怎么还有花费啊??? 别着急,先思考dp的含义。在这里dp的意义是啥???很多题解又是直接给你甩个递推公式,然后稀里糊涂就AC了。抄着抄着自己都懵了。 这里我们看,dp[i]的意义是到达i的最小费用,题目给了一个cost数组,因为要求我们必须到达楼顶,也就是dp数组的最后一个了。 我们看题目给的条件,可以从0或者1开始,那就意味着我们从这花费是0,故dp[0] 和dp[1]都是0, 因为没有跳呢还。那么dp[2]呢?是不是还是和上一题一样,要不从0直接爬两层,加上0层的费用, 要不从1爬一层, 加上1层的费用, 然后我们选取最小值保存。 dp[3]????一个道理大家自己思考下。
d p [ i ] = m i n ( d p [ i − 1 ] + c o s t [ i ] , d p [ i − 2 ] + c o s t [ i − 2 ] ) dp[i] = min(dp[i-1]+ cost[i], dp[i-2]+cost[i-2]) dp[i]=min(dp[i1]+cost[i],dp[i2]+cost[i2])
有了递推公式,我们就可以开始写代码了,再次提问, 从哪里开始?也就是过去的数据在哪里?题目给了cost数组一定是大于2的,所以我们从2开始循环即可。

class Solution {
public:
    int minCostClimbingStairs(vector<int>& cost) {
        vector<int> dp;
        dp.resize(cost.size() + 1);
        
        dp[0] = 0;
        dp[1] = 0;
        for(int i = 2; i <= cost.size(); i++)
        {
            dp[i] = min(dp[i -1] + cost[i -1], dp[i -2] + cost[i - 2]);
        }

        return dp.back();
    }
};

变体:

[62. 不同路径 - 力扣(LeetCode)], 这道题好多同学又懵了,这路径是怎么算数字的啊, 还是个二维数组???别急,按照咱的方法一步步来。首先明确dp是啥意思???在这道题目中,dp就是到达[i][j的最小不同路径和。比如一个2*2的数组,我们想到达坐标 1,1有几种路径,就是dp的含义。 啥意思呢,我举个实际的例子。

你想到达1,1, 题目说了只能向右或者向下走,那么我们是不是得先到1,0或者0,1才能到达1,1啊。那么我们只要把这两种路径加起来就是1,1的值了对吧。那么我们再来考虑,为了到达1,0, 是不是只能从0,0往下走一步,一种路径啊,同样0,1也是一种。因此1,1就是两种。那么我们把二维数组扩大到n*n也是一样的道理。

那我们从哪里开始啊?就是数组的初始化应该怎么办?? 不管到达哪个点,我们是不是只能从最左边的列或者最上面的行来啊,所以我们先把(i, 0)和(0, j)全都初始化成1, 有人问为啥不是0啊,你再想想我们DP的意义就明白了。
d p [ i ] [ j ] = d p [ i − 1 ] [ j ] + d p [ i ] [ j − 1 ] ; dp[i][j] = dp[i-1][j] + dp[i][j -1]; dp[i][j]=dp[i1][j]+dp[i][j1];
​ 这道题要求我们直接到右下角的顶点,所以就直接取二维数组的末位置就可以了。

class Solution {
public:
    int uniquePaths(int m, int n) {
        vector<vector<int>> dp(m, vector<int>(n, 0));
        
        if (m == 1 || n == 1)
            return 1;
        //确定DP的含义 到达[i][j] 位置有多少种路径
        for (int i = 0; i < n; i++)
        {
            dp[0][i] = 1;
        }
        for (int j = 0; j < m; j++)
        {
            dp[j][0] = 1;
        }

        for (int i = 1; i < m; i++)
        {
            for (int j = 1; j < n; j++)
            {
                dp[i][j] = dp[i-1][j] + dp[i][j -1];
            }
        }

        return dp[m - 1][n -1];
    }
};

总结:

DP的核心在于三大块: dp数组含义,递推公式,初始条件。 我们只要把握住,用过去的数据解决现在的问题,就可以拿捏住主干思想。当然以后大家还会碰到更复杂的DP,可能有人见过一些题目,是反向遍历的,为啥可以这么做也蒙了,其实这和我们的递推公式有关。当然这几个初级的题目,完全不需要考虑那么复杂。有的读者可能看到了还有用滚动数组解决的,首先你省的那点内存计算机根本看不上,只能凸显你的代码很秀,如果你真的到那一步了,也不需要看此文了。关于那种进阶的dp问题,我以后还会再深入讲一下。感谢观看。

  • 4
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值