DAY43:不同路径+不同路径Ⅱ(初始化注意点很多,及时复盘)

news2024/9/26 5:15:28

文章目录

    • 62.不同路径(注意初始化)
      • BFS深度搜索写法
      • 动态规划思路
        • DP数组的含义
        • 递推公式
        • DP数组初始化
        • 遍历顺序
        • 打印dp数组
      • 动态规划写法
        • 数组越界的问题
        • for循环执行的问题
    • 63.不同路径Ⅱ(初始化区别)
      • 思路
        • DP数组含义
        • 递推公式
        • DP数组初始化
      • 最开始的写法:初始化有问题
      • 修改完整版
        • for循环遍历条件的问题
      • 总结

62.不同路径(注意初始化)

  • 本题初始化很重要,初始化的同时还要考虑越界的问题与可能性。
  • 这道题需要和不同路径Ⅱ对比来看,就能看出初始化重要性,同时这两道题补充了一些关于for循环终止条件和执行的问题。

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。

问总共有多少条不同的路径?

在这里插入图片描述

输入:m = 3, n = 7
输出:28

示例 2:

输入:m = 3, n = 2
输出:3
解释:
从左上角开始,总共有 3 条路径可以到达右下角。

1. 向右 -> 向下 -> 向下
2. 向下 -> 向下 -> 向右
3. 向下 -> 向右 -> 向下

示例 3:

输入:m = 7, n = 3
输出:28

示例 4:

输入:m = 3, n = 3
输出:6

提示:

  • 1 <= m, n <= 100
  • 题目数据保证答案小于等于 2 * 10^9

BFS深度搜索写法

本题因为是给出了图寻找路径,最直观的想法就是用图论里的深搜,来枚举出来有多少种路径

注意题目中说机器人每次只能向下或者向右移动一步,那么其实机器人走过的路径可以抽象为一棵二叉树,而叶子节点就是终点!

例如下图所示的例子:

在这里插入图片描述
此时问题就可以转化为求二叉树叶子节点的个数,代码如下:

class Solution {
private:
    int dfs(int i, int j, int m, int n) {
        if (i > m || j > n) return 0; // 越界了
        if (i == m && j == n) return 1; // 找到一种方法,相当于找到了叶子节点
        return dfs(i + 1, j, m, n) + dfs(i, j + 1, m, n);
    }
public:
    int uniquePaths(int m, int n) {
        return dfs(1, 1, m, n);
    }
};

但是这么写,提交了代码会发现超时!

实际上是因为这个深搜的算法,其实就是要遍历整个二叉树

这棵树的深度是m+n-1(深度按从1开始计算),那二叉树的节点个数就是 2^(m + n - 1) - 1。可以理解深搜的算法就是遍历了整个满二叉树(其实没有遍历整个满二叉树,只是近似而已)

所以上面深搜代码的时间复杂度为O(2^(m + n - 1) - 1),可以看出,这是指数级别的时间复杂度,是非常大的

再复习一下数据范围和时间复杂度的关系,可以看出,指数级别的时间复杂度是需要n<=30才行的。

在这里插入图片描述

动态规划思路

DP需要记录每一个格子的状态,这样才能用状态转移方程进行格子状态的转换.

因为本题需要记录格子状态,因此我们需要定义一个二维的dp数组

DP数组的含义

二维数组dp[i][j]的含义应该是,从[0,0]位置到[i,j]位置,有多少条不同的路径。因为待求的量就是路径数目。

递推公式

因为题目描述中说,机器人只能往下走或者往右走一步,因此,对于一个位置[i,j],他的上一个位置到达当前位置的路径最多只有两条,就是从上面来和从左边来。如下图所示。

在这里插入图片描述
因此递推公式为:dp[i][j] = dp[i-1][j]+dp[i,j-1]

DP数组初始化

机器人从左上角的[0,0]开始走,由于数组下标越界影响,i和j都需要从1开始,也就是说,只要是i=0,j=0的情况,就会出现-1的下标越界,因此dp[i][0]dp[0][j]这两行都需要做初始化

初始化逻辑:

dp[i][0]就是一路横着走过去,路径数目就是1(因为只有这一个方向,也就是只有一条路径,注意这里是统计路径条数而不是走了几步),dp[0][j]同理

//直接从0开始初始化就行,初始化的时候不需要在意边界条件,[0][0]不存在越界问题
//j是0,横着只有一条路径方向
for(int i=0;i<m;i++){
    dp[i][0]=1;
}
//i是0,竖着也只有一条路径方向
for(int j=0;j<n;j++){
    dp[0][j]=1;
}

遍历顺序

递推公式dp[i][j] = dp[i - 1][j] + dp[i][j - 1]dp[i][j]都是从其上方和左方推导而来,那么从左到右一层一层遍历就可以了。

这样就可以保证推导dp[i][j]的时候,dp[i - 1][j]dp[i][j - 1]一定是有数值的。

打印dp数组

DP数组预期例子如图:

(这里一定要区分走多少步和路径的区别,路径的初始化是同个方向全部为1,但是走多少步的初始化是同个方向为i的递增)

正确的DP数组预期如下图第二个所示。

在这里插入图片描述

动态规划写法

  • 二维数组初始化的方式:先放一维数组个数,再初始化内部的一维数组,如vector<vector<int>>dp(m,vector<int>(n,0))(得到m*n,初值全部为0的二维矩阵)
class Solution {
public:
    int uniquePaths(int m, int n) {
        if(m==0&&n==0) return 0;
        //二维数组初始化的方式:先放一维数组个数,再初始化内部的一维数组
        vector<vector<int>>dp(m,vector<int>(n,0));
        //dp数组初始化
        for(int i=0;i<m;i++){
            dp[i][0]=1;
        }
        for(int j=0;j<n;j++){
            dp[0][j]=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];

    }
};
  • 时间复杂度:O(m × n)
  • 空间复杂度:O(m × n)

数组越界的问题

本题提示里面1 <= m, n <= 100,也就是说m n都是正整数。加上本题递推公式只涉及到了i-1,因此>=1的情况都不会越界。

并且本题是网格前提,m和n要么全部=0,要么都不为0,不存在m和n其中一个是0的情况。

即使存在 m 或 n 为 0, for 循环也不会执行,因为它们是从 0 到 m 或 n 的,所以在这个情况下代码是安全的。

for循环执行的问题

下列初始化的代码,在m=0,n=0的时候,for循环是不会执行的。因为在 C++ 中,for 循环的条件是在每次循环开始之前检查的

假设 m 是 0,那么 for(int i=0; i<m; i++)会立即结束,因为条件 i<m 在一开始就不满足(0不小于0)

同理,如果 n 是 0,那么 for(int j=0; j<n; j++) 也会立即结束,因为条件 j<n 在一开始就不满足。

//dp数组初始化
for(int i=0;i<m;i++){
   dp[i][0]=1;
}
for(int j=0;j<n;j++){
   dp[0][j]=1;
}

因此,即使m和n是0,也不会执行for循环的代码,同样也不会执行递推公式的for循环,因为一开始就不满足i<m的条件。

63.不同路径Ⅱ(初始化区别)

  • 本题和上一题最大区别就在于初始化,本题的初始化需要考虑到,初始化i=0的时候,障碍物的存在会导致后面的全部被堵死,这种情况需要直接break,而不是只置零
  • 而遍历到内部的时候,由于递推公式执行之前先判断是否是障碍物,因此障碍物直接置零,还有其他的路径可以相加,所以continue

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish”)。

现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?

网格中的障碍物和空位置分别用 1 和 0 来表示。

在这里插入图片描述

输入:obstacleGrid = [[0,0,0],[0,1,0],[0,0,0]]
输出:2
解释:3x3 网格的正中间有一个障碍物。
从左上角到右下角一共有 2 条不同的路径:

1. 向右 -> 向右 -> 向下 -> 向下
2. 向下 -> 向下 -> 向右 -> 向右

在这里插入图片描述

输入:obstacleGrid = [[0,1],[0,0]]
输出:1

提示:

  • m == obstacleGrid.length
  • n == obstacleGrid[i].length
  • 1 <= m, n <= 100
  • obstacleGrid[i][j] 为 0 或 1

思路

本题相对于上一题不同路径,实际上只是加上了障碍。本题加上了输入的数组,输入数组里数值为1的,就是有障碍的情况。

本题和上一道题目的基本思路都是相同的,但是我们需要把二维数组中的1变量的dp值全部置零,就可以消除掉障碍物的路径。

DP数组含义

本题DP数组仍然是表示当前累积的路径总数。

递推公式

dp[i][j]=dp[i-1][j]+dp[i][j-1],当有障碍物的时候,赋值dp[i][j]=0

DP数组初始化

在上一道题目中,最开始i=0的一整行,和j=0的一整列,都要初始化成1,因为递推公式里面的i-1和j-1存在下标越界的问题。

但是本题中,我们处理第一行和第一列的时候,一旦有障碍,for循环需要立即break。因为本题只能往右边和下边移动,因此只要有障碍,最上面的第一行后面的路径就全部作废了

但是在二维矩阵内部,如果有障碍可以直接选择continue,因为下一个累加的时候,初始值0不会给累加做贡献。

在这里插入图片描述

最开始的写法:初始化有问题

  • 注意二维数组获得m和n的方法,m直接=r.size(),n=r[0].size()
class Solution {
public:
    int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
        int m=obstacleGrid.size();//得到二维数组一边
        int n=obstacleGrid[0].size();//二维数组另一边
        //cout<<m<<" "<<n<<endl;
        if(m==0&&n==0) return 0;
        //特殊情况:如果起点or终点是1 直接返回
        if (obstacleGrid[m - 1][n - 1] == 1 || obstacleGrid[0][0] == 1) //如果在起点或终点出现了障碍,直接返回0
            return 0;
        vector<vector<int>>dp(m,vector<int>(n,0));//建立相同大小,初始值全是0的DP数组
        //初始化
        for(int i=1;i<m;i++){
            if(obstacleGrid[i][0]==1){
                dp[i][0]=0;
            }
            else{
                dp[i][0]=1;
            }
        }
        for(int j=1;j<n;j++){
            if(obstacleGrid[0][j]==1){
                dp[0][j]=0;
            }
            else{
                dp[0][j]=1;
            }
        }
        //打印DP结果调试
        for(int i=0;i<m;i++){
            for(int j=0;j<n;j++){
                cout<<dp[i][j]<<" ";
            }
             cout<<endl;   
        }
        //递推公式
        for(int i=1;i<m;i++){
            for(int j=1;j<n;j++){
                if(obstacleGrid[i][j]!=1){
                    dp[i][j]=dp[i-1][j]+dp[i][j-1];
                }
                else{
                    dp[i][j]=0;
                }
                //cout<<dp[i][j]<<" ";
            }
        }
        return dp[m-1][n-1];
    }
};

这里的问题在于初始化。

修改完整版

class Solution {
public:
    int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
        int m=obstacleGrid.size();
        int n=obstacleGrid[0].size();
        //如果起点或者终点障碍,直接返回0
        if(obstacleGrid[0][0]==1||obstacleGrid[m-1][n-1]==1) return 0;
        //创建dp数组 m*n矩阵
        vector<vector<int>>dp(m,vector<int>(n,0));
        //dp数组初始化
        for(int i=0;i<m;i++){
            if(obstacleGrid[i][0]==1) break; //如果是障碍物,那么这一行后面都无效,因为只能向右边和下边走
            else
                dp[i][0]=1; 
        }
        for(int j=0;j<n;j++){
            if(obstacleGrid[0][j]==1) break;
            else
                dp[0][j]=1;
        }
        //递推公式
        for(int i=1;i<m;i++){
            for(int j=1;j<n;j++){
                if(obstacleGrid[i][j]==1) continue;
                else{
                    dp[i][j]=dp[i-1][j]+dp[i][j-1];
                }
            }
        }
        return dp[m-1][n-1];
    }
};

for循环遍历条件的问题

本题的初始化代码还可以写成:

vector<vector<int>> dp(m, vector<int>(n, 0));
for (int i = 0; i < m && obstacleGrid[i][0] == 0; i++) dp[i][0] = 1;
for (int j = 0; j < n && obstacleGrid[0][j] == 0; j++) dp[0][j] = 1;

与下面这段代码是等价的。

for (int i = 0; i < m ; i++) {
    if(obstacleGrid[i][0] == 1) break;
    dp[i][0] = 1;
}
for (int j = 0; j < n ; j++) {
    if(obstacleGrid[0][j] == 1) break;
    dp[0][j] = 1;
} 

这是因为for循环的终止条件,一旦遇到obstacleGrid[i][0] == 1的情况就停止dp[i][0]的赋值1的操作

在C++中,for循环的格式是for (initialization; condition; increment)当条件condition为假时,for循环将立即停止,不再执行后面的步骤,这与break语句的作用是一样的

第一种写法的条件部分是 i < m && obstacleGrid[i][0] == 0。当 i < m 是真的,但 obstacleGrid[i][0] == 0 是假的时,这个条件就会是假的,因此for循环会停止。

第二种写法使用了break语句来实现同样的效果。当 obstacleGrid[i][0] == 1 是真的时,break语句会被执行,这导致for循环立即停止,不再执行后面的步骤。

所以说,这是for循环的特性,即它的条件部分可以被看作是每次循环都要检查的一个断言,只有当这个断言是真的时,循环才会继续。当断言变为假时,循环立即终止。这和break语句效果相同

总结

就算是做过62.不同路径,在做本题也会有感觉遇到障碍无从下手。

其实只要考虑到,遇到障碍dp[i][j]保持0就可以了

也有一些重要的初始化细节,例如:初始化的部分,很容易忽略了障碍之后应该都是0的情况

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/738413.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

武汉理工大学信息工程学院2023暑期学术夏令营|信息与通信工程方向

目录 武汉理工大学 校训&#xff1f; 1 通信系统的有效性和可靠性 2 关于QAM调制的问题 2.1 什么是调制 2.2 什么是载波调制 2.3 QAM原理 2.4 数字信号QAM调制 2.5 QAM如何实现&#xff1f; 3 看马路上行驶的车辆轮胎为什么倒转 “视觉暂留” 4 6G/4G/5G的经典技…

卫星数据下载指南

1、USGS Earth Explorer&#xff08;美国地质勘探局地球探测器&#xff09; EarthExplorer 2、Sentinel Open Access Hub https://scihub.copernicus.eu/dhus/#/home 3、NASA Earthdata Search&#xff08;美国航天局地球数据搜索&#xff09; https://search.earthdata.n…

构建新型电力系统,需要什么样的数字能源底座?

在迈向“碳中和”的进程中&#xff0c;全球能源产业变革已经进入了关键时期&#xff0c;其中电力系统的价值从未像今天这样重要。 国际能源署数据显示&#xff0c;2022年全球与能源相关的二氧化碳排放量超过368亿吨&#xff0c;创下历史新高。其中&#xff0c;电力领域碳排放占…

什么是热修复?它的优缺点是什么?

我们开发时常常要考虑的一些问题。 开发上线的版本能保证不存在Bug么&#xff1f; 修复后的版本能保证用户都及时更新么&#xff1f; 如何最大化减少线上Bug对业务的影响&#xff1f; 热修复技术帮助我们解决了很多问题&#xff0c;带来的优势不言而喻。不知道各位对于热修复技…

Qt关于mqtt多平台移植

最近写了很多mqtt相关的项目&#xff0c;但是在各个平台上编译mqtt的库成为了一个很麻烦的事情&#xff0c;每个平台都需要去编译一次&#xff0c;而且还会出现各种问题&#xff0c;为了方便解决这个问题&#xff0c;所以这里将mqtt库的代码以源代码的方式添加进去&#xff0c;…

Kafka 基础概念学习

基础概念学习 一.基础概念二.结构图 一.基础概念 Kafka 基本概念&#xff1a;一种分布式的&#xff0c;基于发布/订阅的消息系统项目ValueBrokerKafka服务端(即部署了Kafka的机器)Producer生产者写数据到主副本Consumer消费者从主副本读数据Consumer Group消费者组&#xff0c…

Apache Doris (二十二) :Doris Rollup物化索引作用及使用注意点

目录 1. Rollup 物化索引作用 1.1 改变索引 1.2 聚合数据 ​​​​​​​2. Rollup物化索引注意点 进入正文之前&#xff0c;欢迎订阅专题、对博文点赞、评论、收藏&#xff0c;关注IT贫道&#xff0c;获取高质量博客内容&#xff01; 宝子们点赞、收藏不迷路&#xff01;…

Stable Diffusion - 图像控制插件 ControlNet (OpenPose) 配置与使用

欢迎关注我的CSDN&#xff1a;https://spike.blog.csdn.net/ 本文地址&#xff1a;https://spike.blog.csdn.net/article/details/131591887 论文&#xff1a;Adding Conditional Control to Text-to-Image Diffusion Models ControlNet 是神经网络结构&#xff0c;用于控制预…

mybatis联查 字段重复问题 查询出来一致

联查的数据是不同&#xff0c;但是返回出来的数据是相同的 排查原因&#xff1a; 两个不同的实体类都有同一个字段code 解决办法&#xff1a; 在数据库字段column字段起个别名就可以

Spring 用注解更简单存取对象

Spring 用注解更简单存取对象 ​ 上一篇文章是最原始的创建使用&#xff0c;这篇主要是讲 Spring 更简单的存储和读取对象的核心是使用注解 ,也是日常生活企业用的最多的方法 “注解” 所以这篇的内容是很重要的 &#xff01;&#xff01;&#xff01; 文章目录 Spring 用注解…

基础篇--单片机简介

单片机简介 视频教程 单片机是什么 单片机&#xff1a;Single-Chip Microcomputer 单片微型计算机&#xff0c;是一种集成电路芯片 单片机有什么用&#xff1f; 单片机发展历程 单片机发展超势 CISC Vs RISC CISC和RISC举例 https://wenku.baidu.com/view/b074b0ed998fcc22b…

浅谈Acrel-2000电力监控系统在配电工程中的应用 安科瑞 许敏

摘 要&#xff1a;随着社会经济的快速发展&#xff0c;我国变电站正朝着现代化的方向不断发展&#xff0c;自动化设备以及继电保护装置凭借自身优异性能而获得广泛应用。本文介绍的AM5SE系列的微机保护装置&#xff0c;可以针对中原科技城智慧能源配电工程中不同保护对象提供对…

全志f1c200s嵌入式驱动开发(原理图)

【 声明:版权所有,欢迎转载,请勿用于商业用途。 联系信箱:feixiaoxing @163.com】 熟悉全志soc的同学都知道,除了v3s之外,其实还有很多性价比很高的soc。这里面就包含了f1c200s。之前的v3s,也就是我们所说的licheepi上面的soc,反响很不错。后期又出了一个liche…

【数据结构与算法】力扣:二叉树的层序遍历

给你二叉树的根节点 root &#xff0c;返回其节点值的 层序遍历 。 &#xff08;即逐层地&#xff0c;从左到右访问所有节点&#xff09;。 示例1&#xff1a; 输入&#xff1a;root [3,9,20,null,null,15,7] 输出&#xff1a;[[3],[9,20],[15,7]] 示例 2&#xff1a; 输入&a…

纷享销客携手新奥动力,赋能能源装备制造业企业增长再提速

近日&#xff0c;纷享销客与新奥能源动力科技&#xff08;上海&#xff09;有限公司&#xff08;以下简称&#xff1a;新奥动力&#xff09;售后数字化平台项目启动仪式在上海顺利召开&#xff0c;新奥动力和纷享销客双方多位高管共同出席了当天的启动会&#xff0c;双方就CRM建…

leetcode链表刷题记录

题单&#xff1a; 一&#xff0c;移除链表元素 题目描述 给你一个链表的头节点 head 和一个整数 val &#xff0c;请你删除链表中所有满足 Node.val val 的节点&#xff0c;并返回 新的头节点 。 题目接口&#xff1a; /*** Definition for singly-linked list.* struct Lis…

轻量化与强度:挑选骑行爱车的科学平衡之道

在自行车运动的大家庭中&#xff0c;挑选一辆既轻量化又强度又够的自行车是一项关键的任务。那么&#xff0c;如何在轻量化与强度的矛盾中寻找这个科学的平衡点呢&#xff1f;让我们跟随本文&#xff0c;一起探讨如何挑选骑行爱车的科学之道。 首先&#xff0c;我们需要明确&am…

第4章 SSD核心技术:FTL 4.3-4.5

4.3 垃圾回收 4.3.1 垃圾回收原理 WA&#xff08;Write Amplification&#xff09;&#xff0c;即写放大。 OP&#xff08;Over Provisioning&#xff09;&#xff0c;即预留空间。 4.3.2 写放大 对于空盘来说&#xff08;未触发GC&#xff09;&#xff0c;写放大一般为1&…

基于DarkFace数据集的高精度人脸检测系统(PyTorch+Pyside6+YOLOv5模型)

摘要&#xff1a;基于DarkFace数据集的高精度人脸检测系统可用于日常生活中来检测与定位人脸目标&#xff0c;利用深度学习算法可实现图片、视频、摄像头等方式的人脸目标检测识别&#xff0c;另外支持结果可视化与图片或视频检测结果的导出。本系统采用YOLOv5目标检测模型训练…

volatile 原理

volatile 的底层实现原理是内存屏障&#xff0c;Memory Barrier&#xff08;Memory Fence&#xff09; 对 volatile 变量的写指令后会加入写屏障对 volatile 变量的读指令前会加入读屏障 如何保证可见性 写屏障&#xff08;sfence&#xff09;保证在该屏障之前的&#xff0c…