【算法】C++动态规划+滚动数组(面试极其加分的代码)

一、DP思路

  • 看到OJ题需要求最值,就应该形成条件反射——嗯,这题或许要使用贪心算法或者动态规划。当你发现贪心不能解决问题,且原问题规模可缩小时,即可考虑用动态规划求解

  • 先把原问题的规模缩成最小的子问题,确定边界

  • 逐步扩大子问题,并且通过前面步骤的子问题的解来求最新子问题的解

  • 当子问题规模扩大为i时,可推出状态转移方程

例如:原问题dp[i]的解=子问题dp[i-1]的解+子问题dp[i-2]的解

  • dp[i]数组的意义:问题变量的状态。随着i的变化dp[i]记录着问题变量状态的变化过程

二、题解步骤

  1. 确定原问题与子问题
  2. 确认状态,dp[i]数组的意义
  3. 确认边界状态的值
  4. 确认状态转移方程

(理论晦涩难懂,请看下方例题,长文警告!!!)

三、例题

动态规划最经典的问题莫过于背包问题
想要了解的朋友可点击下方的3个传送门
01背包
小破站视频讲解
dalao博客
完全/多重背包
dalao博客

而我更想讲的是下面这题——一道比较简单易懂的dp题

一个机器人位于一个 m x n 网格的左上角,机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角。现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?

  • 网格中的障碍物和空位置分别用 1 和 0 来表示。
  • m和n的值均不超过100
    来源:力扣(LeetCode)力扣每日一题-20200706

动态规划的题目分为两大类,一种是求最优解类(就是上文所提到的条件反射),典型问题是背包问题,另一种就是计数类,比如这里的统计方案数的问题,它们都存在一定的递推性质。

假设,我们要从左上角的起点a走到右下角的终点b,每次只能向右或者向下移动1格,问:总共有多少条不同路径(暂时不考虑障碍问题)
如下图:
在这里插入图片描述
确定原问题:

首先明确:我们的终极目标就是要到b处(从起点走到终点),这就是我们的原问题,这应该很好理解

确定子问题:

  • 由图和题目可知,我们要走到方格9(b处),那么唯有从方格6或者方格8处直接到达(一次只能向下或者向右移动一步
  • 这个时候我们如果先把问题的范围稍微缩小一下,把终极目标改为走到6处
  • 同理可得,6可由3或者5直接到达,再次缩小范围,依次类推…3只能由2到达,而2只能由1到达…当我们来到这一步的时候,我们的最小子问题就出来了:有多少路径可由1到达2?(也可以理解为从1到达1才是最小子问题)显而易见只有1条,同理,由1到达4也只有1条路径

逐步扩大子问题:

那么如今我们

  • 已知1到2与1到4的路径总和,是不是能求1到5的路径总和了?
  • 已知1到2的路径总和,是不是能求1到3的路径总和了?
  • 已知1到3和1到5的路径总和,是不是能求1到6的路径总和了?

  • 依次类推,我们最终能得到1到6和1到8的路径总和,进而求得我们的终极目标——从a到b处的路径总和

根据我们逐步扩大子问题的过程,我们可以列出下面公式:

cnt[i]:从方格1走到方格i的路径总和

已知1到2与1到4的路径总和,求1到5的路径总和:cnt[5] = cnt[2]+cnt[4];
已知1到2,求1到3的路径总和:cnt[3] = cnt[2];
已知1到3与1到5的路径总和,求1到6的路径总和:cnt[6] = cnt[3]+cnt[5];


已知1到6与1到8的路径总和,求1到9的路径总和:cnt[9] = cnt[6]+cnt[8];

dp[i]的意义:
cnt[i]:从方格1走到方格i的路径总和
此处的 cnt[i] 即我们前文所提到的 dp[i]
从方格1走到方格i的路径总和 即为 dp[i]的意义

我们再把上述公式化为二维进行表示,即:

cnt[1][1] = cnt[0][1]+cnt[1][0];
cnt[0][2] = cnt[0][1];
cnt[1][2] = cnt[1][1]+cnt[0][2];


cnt[2][2] = cnt[1][2]+cnt[2][1];

到了这一步,我们可以发现一个规律:

cnt[ i ] [ j ] = cnt[ i - 1] [ j ] + cnt[ i ] [ j - 1];

这就是我们前文所提到的状态转移方程 ,其实所谓的状态转移方程是可以理解为题目所遵循的一个规律,就像一个二元一次方程y=2x+1一样,结果y是随着参数x的变化而变化的,而我们这里的cnt[ i - 1] [ j ] 和cnt[ i ] [ j - 1 ] 也影响着cnt[ i ] [ j ]

依据状态转移方程,从起点(i=0,j=0)出发,慢慢地往下递推,即可求出到达其他所有点的路径总和
至于障碍什么的,加个if就🆗啦
代码如下:(要仔细看注释哦,注释的颜色有点浅emm…)

class Solution {
public:
    int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
        //初始化dp数组,此处dp数组即为上文提到的cnt数组
        int dp[100][100] = {0};
        if(!obstacleGrid.size()) return 0;
        int m = obstacleGrid.size(),n = obstacleGrid[0].size();
		
		//核心代码:双重for循环遍历原地图
        for(int i = 0;i < m;i++){
            for(int j = 0;j < n;j++){
	            //up:从起点到obstacleGrid[i][j]的上面方格的路径和
	      		//left:从起点到obstacleGrid[i][j]的左边方格的路径和
	      		//obstacleGrid[i][j] = up + left
                int up = 0,left = 0;
                //判断越界!!!
                if(i-1>=0 && obstacleGrid[i-1][j]!=1) up = dp[i-1][j];
                if(j-1>=0 && obstacleGrid[i][j-1]!=1) left = dp[i][j-1];
                //判断障碍
                if(obstacleGrid[i][j]!=1) 
                    if(i==0 && j==0) dp[i][j] = 1;
                    else dp[i][j] = up+left;                
            }
        }
        //理解了此题dp数组的意义,返回值自然就是小case了
        return dp[m-1][n-1];
    }
};

看到这里,如果你对这题已经大概理解,也对dp已经有了些许的感觉,就可以继续看下方👇的官方题解(解题思路基本一样)

官方题解:

class Solution {
public:
    int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
        int n = obstacleGrid.size(), m = obstacleGrid.at(0).size();
        vector <int> f(m);

        f[0] = (obstacleGrid[0][0] == 0);
        for (int i = 0; i < n; ++i) {
            for (int j = 0; j < m; ++j) {
                if (obstacleGrid[i][j] == 1) {
                    f[j] = 0;
                    continue;
                }
                if (j - 1 >= 0 && obstacleGrid[i][j - 1] == 0) {
                    f[j] += f[j - 1];
                }
            }
        }
        return f.back();
    }
};

//作者:LeetCode-Solution
//链接:https://leetcode-cn.com/problems/unique-paths-ii/solution/bu-tong-lu-jing-ii-by-leetcode-solution-2/
//来源:力扣(LeetCode)
//著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

咦?好像有点不对? 如果细心看完这2个题解代码的朋友,就会发现:

菜鸡的代码:空间复杂度O(mn)
官方的代码:空间复杂度O(m)

这就是接下来要提到的DP问题空间复杂度优化了
(建议先理解了这题的dp思想,再去理解滚动数组)

四、滚动数组初体验(DP问题空间优化)

滚动数组,通俗来说,就是让数组滚动起来,用更小的空间去存储数据。
看回咱们这题,我们的状态转移方程

cnt[ i ] [ j ] = cnt[ i - 1] [ j ] + cnt[ i ] [ j - 1];

可以看出cnt[ i ] [ j ]只与cnt[ i - 1] [ j ]和cnt[ i ] [ j -1]这两个数有关,那么如果我们开辟出m*n的空间去把所有的cnt的值都存起来,是不是显得有点浪费呢?

一维的滚动数组中,我们把cnt[ i ] [ j ] 的内容放到了cnt[ i ] 中,此时cnt[ i ]的意义就是当前行(第j行)的第i个元素的值,因为在我们的两份代码中,都是一个双重for循环,一行一行地对map进行迭代,而我们的cnt[ i ] 数组就随着我们的迭代进行滚动,代表着当前行第i个元素的值

cnt[ i ] [ j ] 是等于 cnt[ i -1] [ j ] 加上 cnt[ i ] [ j -1] 的,而转换到一维数组中:cnt[ i ] 就是等于 cnt[ i - 1] 加上 cnt[ i ]
对,没错,就是加上他自己本身原来所存储的值
因为当你要计算cnt[ i ] [ j ] 的值时,cnt[ i ] 存储的其实还是上一行cnt[ i ] 的值,即cnt[ i ] [ j - 1 ] 的值
(请结合下图理解)
在这里插入图片描述
如果在手撕代码的时候,不需要面试官提醒就写出了带空间优化的代码,可是会加分的哦!

  • 5
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值