代码随想录第三十九天 | 不同路径:没有障碍:深搜,动态规划,数论(62);有障碍:明确动规数组含义 以及 初始化 的重要性(63)

1、没有障碍的不同路径:深搜,动态规划,数论

1.1 leetcode 62:不同路径

第一遍代码
record 记录起点到这个格子几条路径
只有左边一格上面一格有可能能到目标格子

class Solution {
public:
    int uniquePaths(int m, int n) {
        vector<vector<int>> record(m, vector<int>(n));
        //record 记录从起点到这个格子有几条路径
        for(int i = 0; i < n; i++) {
            record[0][i] = 1;
        }
        for(int j = 0; j < m; j++) {
            record[j][0] = 1;
        }
        for(int i = 1; i < n; i++) {
            for(int j = 1; j < m; j++) {
                record[j][i] = record[j - 1][i] + record[j][i - 1];
                //只有左边一格,上面一格有可能能到这个格子
            }
        }
        return record[m - 1][n - 1];
    }
};

1.2 leetcode 62:深搜

这道题目,刚一看最直观的想法就是用图论里的深搜,来枚举出来有多少种路径
注意题目中说机器人每次只能向下或者向右移动一步,那么其实机器人走过的路径可以抽象为一棵二叉树,而叶子节点就是终点

如图举例:
代码随想录第三十九天 | 不同路径:没有障碍:深搜,动态规划,数论(62);有障碍:明确动规数组含义 以及 初始化 的重要性(63)_第1张图片
此时问题就可以转化为求二叉树叶子节点的个数叶子节点为终点根节点为初始节点,代码如下(其实不好想):

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开始计算),可以理解深搜的算法就是遍历了整个满二叉树(其实没有遍历整个满二叉树,只是近似而已),所以上面深搜代码的时间复杂度O(2m+n-1 -1),可以看出,这是指数级别的时间复杂度,是非常大的,也超时

1.3 leetcode 62:动态规划(第一遍代码思路)

机器人从(0 , 0) 位置出发,到(m - 1, n - 1)终点

按照动规五部曲来分析:
1、确定dp数组(dp table)以及下标的含义
dp[i][j] :表示从(0 ,0)出发到(i, j)dp[i][j]条不同的路径

2、确定递推公式
想要求dp[i][j],只能有两个方向来推导出来,即dp[i - 1][j] 和 dp[i][j - 1]
此时在回顾一下 dp[i - 1][j] 表示啥,是从(0, 0)的位置到(i - 1, j)有几条路径,dp[i][j - 1]同理,那么很自然,dp[i][j] = dp[i - 1][j] + dp[i][j - 1],因为dp[i][j]只有这两个方向过来

3、dp数组的初始化
如何初始化呢,首先dp[i][0]一定都是1,因为从(0, 0)的位置到(i, 0)的路径只有一条,那么dp[0][j]也同理

所以初始化代码为:

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

4、确定遍历顺序
这里要看一下递推公式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]一定是有数值

5、举例推导dp数组
如图所示:
代码随想录第三十九天 | 不同路径:没有障碍:深搜,动态规划,数论(62);有障碍:明确动规数组含义 以及 初始化 的重要性(63)_第2张图片
以上动规五部曲分析完毕,C++代码如下:

class Solution {
public:
    int uniquePaths(int m, int n) {
        vector<vector<int>> dp(m, vector<int>(n, 0));
        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)

其实用一个一维数组(也可以理解是滚动数组)就可以了,就是把多行并在一起下一行直接在上一行的基础上加就行了(反正在二维数组中也需要加上上一行信息),C++代码如下:

class Solution {
public:
    int uniquePaths(int m, int n) {
        vector<int> dp(n);
        for (int i = 0; i < n; i++) dp[i] = 1;
        for (int j = 1; j < m; j++) {
            for (int i = 1; i < n; i++) {
                dp[i] += dp[i - 1];
            }
        }
        return dp[n - 1];
    }
};

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

1.4 leetcode 62:数论方法

在这个图中,可以看出一共m,n的话,无论怎么走,走到终点都需要 m + n - 2 步
代码随想录第三十九天 | 不同路径:没有障碍:深搜,动态规划,数论(62);有障碍:明确动规数组含义 以及 初始化 的重要性(63)_第3张图片
这m + n - 2 步中,一定有 m - 1 步是要向下走的不用管什么时候向下走
那么有几种走法呢? 可以转化为,给你m + n - 2个不同的数随便取m - 1个数,有几种取法,那么这就是一个组合问题

那么答案,如图所示:
代码随想录第三十九天 | 不同路径:没有障碍:深搜,动态规划,数论(62);有障碍:明确动规数组含义 以及 初始化 的重要性(63)_第4张图片
求组合的时候,要防止两个int相乘溢出! 所以不能把算式的分子都算出来分母都算出来再做除法

例如如下代码是不行的

class Solution {
public:
    int uniquePaths(int m, int n) {
        int numerator = 1, denominator = 1;
        int count = m - 1;
        int t = m + n - 2;
        while (count--) numerator *= (t--); // 计算分子,此时分子就会溢出
        for (int i = 1; i <= m - 1; i++) denominator *= i; // 计算分母
        return numerator / denominator;
    }
};

需要在计算分子的时候,不断除以分母

根据思路实现代码:
分母连乘只要能整除 且 不为0就可以先除

class Solution {
public:
    int uniquePaths(int m, int n) {
        long long fenzi = 1;
        int fenmu = m - 1;
        int fenzi_num = m + n - 2;
        while(fenzi_num > m + n - 2 - m + 1) {
            //控制分子乘的数
            fenzi *= fenzi_num;
            fenzi_num--;
            while(fenmu != 0 && fenzi % fenmu == 0) {
                //只要能 整除 不为0就可以先除
                fenzi /= fenmu;
                fenmu--;
            }
        }
        return fenzi;
    }
};

代码随想录代码如下:

class Solution {
public:
    int uniquePaths(int m, int n) {
        long long numerator = 1; // 分子
        int denominator = m - 1; // 分母
        int count = m - 1;
        int t = m + n - 2;
        while (count--) {
            numerator *= (t--);
            while (denominator != 0 && numerator % denominator == 0) {
                numerator /= denominator;
                denominator--;
            }
        }
        return numerator;
    }
};

时间复杂度:O(m)
空间复杂度:O(1)

1.5 leetcode 62:总结

本文分别给出了深搜,动规,数论三种方法
深搜当然是超时了,顺便分析了一下使用深搜的时间复杂度,就可以看出为什么超时了
然后在给出动规的方法,依然是使用动规五部曲,这次我们就要考虑如何正确的初始化了,初始化和遍历顺序其实也很重要

2、有障碍的不同路径:明确动规数组含义 以及 初始化 的重要性

2.1 leetcode 63:不同路径 II

第一遍代码
有障碍物就归0,使用dp数组记录从起点到当前节点几条路径递推方程与 leetcode 62 一致
行列初始化变了,只要前面 / 上面有障碍物,就没办法通过

class Solution {
public:
    //有障碍物就归0,使用dp数组记录从起点到当前节点有几条路径,递推方程与62一致(要时刻记住dp数组代表的什么意思)
    int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
        int dp[obstacleGrid.size()][obstacleGrid[0].size()];
        for(int i = 0; i < obstacleGrid.size(); i++) {
            if(obstacleGrid[i][0] == 0) {
            //行列初始化变了,只要前面 / 上面有障碍物,就没办法通过
                if(i > 0 && dp[i - 1][0] == 0) {
                    dp[i][0] = 0;
                }
                else {
                    dp[i][0] = 1;
                }
            }
            else {
                dp[i][0] = 0;
            }
        }
        for(int j = 0; j < obstacleGrid[0].size(); j++) {
            if(obstacleGrid[0][j] == 0) {
                if(j > 0 && dp[0][j - 1] == 0) {
                    dp[0][j] = 0;
                }
                else {
                    dp[0][j] = 1;
                }
            }
            else {
                dp[0][j] = 0;
            }
        }
        for(int i = 1; i < obstacleGrid.size(); i++) {
            for(int j = 1; j < obstacleGrid[0].size(); j++) {
                if(obstacleGrid[i][j] == 0) {
                    dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
                }
                else {
                    dp[i][j] = 0;
                }
            }
        }
        return dp[obstacleGrid.size() - 1][obstacleGrid[0].size() - 1];
    }
};

代码随想录思路
leetcode 62 中我们已经详细分析了没有障碍的情况有障碍的话,其实就是标记对应的dp tabledp数组保持初始值(0) 就可以了

动规五部曲
1、确定dp数组(dp table)以及下标的含义
dp[i][j] :表示从(0,0)出发,到(i, j) 有dp[i][j]条不同的路径

2、确定递推公式
递推公式和 leetcode 62:不同路径 一样dp[i][j] = dp[i - 1][j] + dp[i][j - 1]
但这里需要注意一点,因为有了障碍(i, j)如果就是障碍的话应该就保持初始状态(初始状态为0)

所以代码为:

if (obstacleGrid[i][j] == 0) { // 当(i, j)没有障碍的时候,再推导dp[i][j]
    dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}

3、dp数组如何初始化
如果**(i, 0) 这条边有了障碍之后,障碍之后(包括障碍)都是走不到的位置了,所以障碍之后dp[i][0]应该还是初始值0**
如图:
代码随想录第三十九天 | 不同路径:没有障碍:深搜,动态规划,数论(62);有障碍:明确动规数组含义 以及 初始化 的重要性(63)_第5张图片
下标(0, j)初始化情况同理

所以本题初始化代码为:

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循环的终止条件,一旦遇到 obstacleGrid[i][0] == 1 的情况停止 dp[i][0] 的赋值1的操作dp[0][j]同理(妙)

4、确定遍历顺序
从递归公式 dp[i][j] = dp[i - 1][j] + dp[i][j - 1] 中可以看出,一定是从左到右一层一层遍历,这样**保证推导dp[i][j]**的时候,dp[i - 1][j] 和 dp[i][j - 1]一定是有数值

代码如下:

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

5、举例推导dp数组
示例1来举例如题:
代码随想录第三十九天 | 不同路径:没有障碍:深搜,动态规划,数论(62);有障碍:明确动规数组含义 以及 初始化 的重要性(63)_第6张图片
对应的dp table 如图:
代码随想录第三十九天 | 不同路径:没有障碍:深搜,动态规划,数论(62);有障碍:明确动规数组含义 以及 初始化 的重要性(63)_第7张图片
动规五部分分析完毕,对应C++代码如下:

class Solution {
public:
    int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
        int m = obstacleGrid.size();
        int n = obstacleGrid[0].size();
	if (obstacleGrid[m - 1][n - 1] == 1 || obstacleGrid[0][0] == 1) //如果在起点或终点出现了障碍,直接返回0
            return 0;
        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 = 1; i < m; i++) {
            for (int j = 1; j < n; j++) {
                if (obstacleGrid[i][j] == 1) continue;
                dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
            }
        }
        return dp[m - 1][n - 1];
    }
};

时间复杂度:O(n × m),n、m 分别为obstacleGrid 长度和宽度
空间复杂度:O(n × m)

同样我们给出空间优化版本

class Solution {
public:
    int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
        if (obstacleGrid[0][0] == 1)
            return 0;
        vector<int> dp(obstacleGrid[0].size());
        for (int j = 0; j < dp.size(); ++j)
            if (obstacleGrid[0][j] == 1)
                dp[j] = 0;
            else if (j == 0)
                dp[j] = 1;
            else
                dp[j] = dp[j-1];

        for (int i = 1; i < obstacleGrid.size(); ++i)
            for (int j = 0; j < dp.size(); ++j){
                if (obstacleGrid[i][j] == 1)
                    dp[j] = 0;
                else if (j != 0)
                    dp[j] = dp[j] + dp[j-1];
            }
        return dp.back();
    }
};

按照思路自己实现在一维数组上的版本

初始化只要考虑行上有障碍物的情况,因为对于列上的障碍物只要前面行上同样列有1,同时判断一下这一个位置是否有障碍物(别漏了) 加的时候自然为0
obstacleGrid[0][i]二维数组下标别写漏了
第一列也要判断障碍物

class Solution {
public:
    int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
        vector<int> dp(obstacleGrid[0].size(), 0);
        if(obstacleGrid[0][0] == 1) return 0;
        //初始化只要考虑行上有障碍物的情况,因为对于列上的障碍物,只要前面行上同样列有1,同时判断一下这一个位置是否有障碍物(别漏了) 加的时候自然为0
        for(int i = 0; i < obstacleGrid[0].size() && obstacleGrid[0][i] == 0; i++) {//obstacleGrid[0][i]二维数组别写错了
            dp[i] = 1;
        }
        for(int i = 1; i < obstacleGrid.size(); i++) {
            for(int j = 0; j < obstacleGrid[0].size(); j++) {
                if(obstacleGrid[i][j] == 1) {//第一列也要判断障碍物
                    dp[j] = 0;
                    continue;
                }
                if(j > 0) {
                    dp[j] = dp[j] + dp[j - 1];
                }
            }
        }
        return dp[obstacleGrid[0].size() - 1];
    }
};

2.2 leetcode 63:总结

其实只要考虑到,遇到障碍dp[i][j]保持0就可以了
也有一些小细节,例如:初始化的部分,很容易忽略了障碍之后应该都是0的情况

你可能感兴趣的:(leetcode,c++,动态规划,算法,c++,leetcode)