C++动态规划理解

   日期:2020-07-08     浏览:95    评论:0    
核心提示:动态规划解题步骤思路例题解题步骤动态规划解题步骤:确定原问题与子问题确认状态,dp[i]数组的意义确认边界状态的值确认状态转移方程思路看到OJ题求最值,一般使用贪心或者动态规划。如果贪心不能解而且原问题规模可缩小,可用动态规划求解先把原问题的规模缩成最小的子问题,确定边界逐步扩大子问题,并且通过前面步骤的子问题的解来求最新子问题的解当子问题规模扩大为i时,可推出状态转移方程例如:原问题dp[i]的解=子问题dp[i-1]的解+子问题dp[i-2]的解d

动态规划(Dynamic Programing)

  • DP思路
  • 题解步骤
  • 例题
  • 滚动数组初体验(DP问题空间优化)

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 ] 的值
(请结合下图理解)

 
打赏
 本文转载自:网络 
所有权利归属于原作者,如文章来源标示错误或侵犯了您的权利请联系微信13520258486
更多>最近资讯中心
更多>最新资讯中心
0相关评论

推荐图文
推荐资讯中心
点击排行
最新信息
新手指南
采购商服务
供应商服务
交易安全
关注我们
手机网站:
新浪微博:
微信关注:

13520258486

周一至周五 9:00-18:00
(其他时间联系在线客服)

24小时在线客服