动态规划 第0讲:基础入门课

Leetcode算法笔记 专栏收录该内容
15 篇文章 1 订阅

参考博文:
1、经典中的经典算法:动态规划(详细解释,从入门到实践,逐步讲解) 作者:BS有前途
2、告别动态规划,连刷40道动规算法题,我总结了动规的套路(微信公众号:帅地玩编程)
3、LeetCode:120 三角形最小路径和 动态规划解法 O(n)空间 作者:AkagiSenpai

一、动态规划的定义:

维基百科:
动态规划(Dynamic Programming, DP)在查找有很多重叠子问题的情况的最优解时有效。它将问题重新组合成子问题。为了避免多次解决这些子问题,它们的结果都逐渐被计算并被保存,从简单的问题直到整个问题都被解决。因此,动态规划保存递归时的结果,因而不会在解决同样的问题时花费时间。
动态规划只能应用于有最优子结构的问题。最优子结构的意思是局部最优解能决定全局最优解(对有些问题这个要求并不能完全满足,故有时需要引入一定的近似)。

动态规划算法是通过拆分问题定义问题状态和状态之间的关系,使得问题能够以递推的方式去解决。
动态规划算法的基本思想与分治法类似,也是将待求解的问题分解为若干个子问题(阶段),按顺序求解子阶段,前一子问题的解,为后一子问题的求解提供了有用的信息。在求解任一子问题时,列出各种可能的局部解,通过决策保留那些有可能达到最优的局部解,丢弃其他局部解。依次解决各子问题,最后一个子问题就是初始问题的解。

二、基本思想与解题策略:

动态规划,无非就是利用历史记录,来避免我们的重复计算。而这些历史记录,我们得需要一些变量来保存,一般是用一维数组或者二维数组来保存。
1、首先是拆分问题,就是根据问题的可能性,把问题划分成一步一步。这样就可以通过递推或者递归来实现。
关键就是这个步骤,动态规划有一类问题就是从后往前推。有时候我们很容易知道:如果只有一种情况时,最佳的选择应该怎么做。然后根据这个最佳选择往前一步推导,得到前一步的最佳选择。
2、找出数组元素之间的关系式,我觉得动态规划,还是有一点类似于我们高中学习时的归纳法的,当我们要计算 dp[n] 时,是可以利用 dp[n-1],dp[n-2],…,dp[1],来推出 dp[n] 的,也就是可以利用历史数据来推出新的元素值,所以我们要找出数组元素之间的关系式,例如 dp[n] = dp[n-1] + dp[n-2],这个就是他们的关系式了。而这一步,也是最难的一步,后面我会讲几种类型的题来说。
3、为减少重复计算,对每一个子问题只解一次(重点)将其不同阶段的不同状态保存在一个二维数组中。比如我们找到最优解,我们一个先将最优解保存下来,为了往前推导时能够使用前一步的最优解,在这个过程中难免有一些相比于最优解差的解,此时我们应该放弃,只保存最优解,这样我们每一次都把最优解保存了下来,大大降低了时间复杂度。

三、详细代码分析、推导、改进

话不多说,咱们来看一道经典例题(Leetcode 120.Triangle)

Given a triangle, find the minimum path sum from top to bottom. Each step you may move to adjacent numbers on the row below.

For example, given the following triangle

[
     [2],
    [3,4],
   [6,5,7],
  [4,1,8,3]
]
The minimum path sum from top to bottom is 11 (i.e., 2 + 3 + 5 + 1 = 11).

Note:

Bonus point if you are able to do this using only O(n) extra space, where n is the total number of rows in the triangle.

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/triangle
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

这道题是找最小和,只能从左下角和右下角加上去。
但是,下面因为借鉴了一篇博文,为了少写一下,我就搬运他的部分内容过来了。他是最大值,性质是一样的,并不影响对动态规划的理解。

下面我们开始来分析了,

代码 Vision-1:

注:这个版本的代码是最初版本的代码,之所以放上来,是为了方便理解动态规划。如果放到Leetcode,是超时的。
可以看出每走第n行第m列时有两种后续:向下(i+1,j)或者向右下(i+1,j+1)
最后一行可以当做边界条件,所以我们自然而然想到递归求解。

使用动态规划求解的基本思路
放代码看一看(C 语言,因为现在这个版本的代码比较基础,暂时没必要用到C++):
Vision 1

代码 Vision-2:

如果仔细观察,Vision-1的解答过程时间复杂度难以想象的大,那是因为他对有的数字的解进行了多次的重复计算,具体原因如下图:
Vision 1 超时的原因
然后我们就可以自然而然的想到,如果我们每次都把结果保存下来(保存在一个二维数组中) 重点!!!!认真看这里!!!!,复杂度就会大大降低。就可以把时间复杂度降低成O(n^2),因为三角形的数字总和为 n*(n+1)/2.

好,我们来看一下代码:Vision 2

代码Vision-3

还可以咋改进呢?众所周知,递归一般情况下是可以转化为递推的,直接贴代码来看一看:
(递推:从后往前(这道题即从n开始),也就是说从递归最小的那层开始)
Vision 3

代码 Vision-4

回归到Leetcode 120.Triangle这道题。如果用上面的代码去写,就会出错(如下图),因为测试数据不只有几百行,而是很多很多。C语言的数组好像不太好用了,那我们就用C++ std的vector(即std::vector<std::vector> dp)
在这里插入图片描述
Vision-4 的代码:

class Solution {
public:
    int minimumTotal(vector<vector<int>>& triangle) {
        int n= triangle.size();
        if(n==0) return 0;
        
        std::vector<std::vector<int>> dp;//设置dp二维数组
        for(int i=0; i<n; i++)
        {
            dp.push_back(std::vector<int>());
            for(int j=0; j<=i; j++)
            dp[i].push_back(0);
        }

        for(int j=0; j<n; j++)
        dp[n-1][j]= triangle[n-1][j];//边界是三角形最后一层,每一个点的最优解就是自身

        for(int i= n-2; i>=0; i--)
        {  //从倒数第二层dp到第0层
            for(int j= 0; j<=i; j++)
            dp[i][j]= std::min(dp[i+1][j], dp[i+1][j+1]) + triangle[i][j];   //每一行分别进行dp
        }
        return dp[0][0];
    }
};
代码 Vision-5

Vision-4是可以勉强通过,但是太慢了(看下图)
Vision 4的太慢了
我们也可以对动态规划进行空间压缩,起到节省空间消耗的效果。
可不可以改进一下呢?空间复杂度可不可以改成O(n)呢?肯定是可以的。
因为题目说了,Bonus point if you are able to do this using only O(n) extra space, where n is the total number of rows in the triangle.
仔细思考一下,其实根本就不用存所有的点, 或者说有些点用完后就不会在用了。比如我算倒数第三层, 那么倒数第一层的值就用不到了, 所以我们只需维护两个个大小为N数组,保存当前层和上一层,就可以优化到O(n)了!

class Solution {
public:
    int minimumTotal(vector<vector<int>>& triangle) {
        int n= triangle.size();
        if(n==0) return 0;
        
        std::vector<int> dp = triangle[n-1];//一维数组,将最后一行作为初始值

        for(int i= n-2; i>=0; i--)
        {
            for(int j=0; j<=i; j++)
            dp[j]= triangle[i][j] + std::min(dp[j], dp[j+1]);
        }
        return dp[0];
    }
};
代码 Vision-6

经过改了几次代码,终于到达O(n)了,执行速度也快了不少。
快了一点,也实现O(n)了
但是有没有更好的方法呢?
肯定是有的,但是我今天晚上不想写了,到时候再补充吧。

  • 2
    点赞
  • 0
    评论
  • 0
    收藏
  • 打赏
    打赏
  • 扫一扫,分享海报

参与评论 您还未登录,请先 登录 后发表或查看评论
©️2022 CSDN 皮肤主题:大白 设计师:CSDN官方博客 返回首页

打赏作者

SG_Dreaming

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值