34.动态规划(2) | 不同路径、不同路径 II

        今天是两道路径相关的DP题目,之前接触过,所以做起来比较容易。两道题虽然原理和实现都比较容易,但想优化成用一行数组代替矩阵的实现方法还是有一些坑的。第1道题比较经典。从中学习到了3种完全不同的,时间复杂度由高到低的方法,以及计算组合数的数论模板方法。从第2道题学习补充了一些容易疏忽的漏洞。


        第1题(LeetCode 62. 不同路径)比较简单,自己的思路是把dp[i][j]定义为到达迷宫的第i行第j列可能的路径数。而由于只能向下或向右走,那么对于第i行第j列,它的来源只可能是它的上方或左方,也就是第i - 1行第j列,或者第i行第j - 1列。那么把这两者的对应路径数相加,就能得到当前点的路径数。所以状态转移方程为dp[i][j] = dp[i][j - 1] + dp[i - 1][j]。初始化方面,由于第0行的所有点都是只能由出发点向右走的,第0列所有点都是只能由出发点向下走的,所以第0行和第0列的值都应该是1。于是就可以从第1行开始便利到第m - 1行。每行从第1列开始遍历到第n - 1列。最终dp矩阵右下角的数值就是答案。

        又因为更新每一行时,只会用到上一行和当前位置左边的数字,所以并不需要一个矩阵的空间,仅需要一行的空间就可以了。于是优化后的代码如下:

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

于是这种方法的时间复杂度是O(mn),空间复杂度是O(n)。

        题解中,首先介绍的是DFS方法。如果使用搜索的方法,那么搜索树的深度就是m + n - 1(深度按从1开始计算),二叉树的节点数就是2^(m+n-1)了,对应时间复杂度O( 2^(m+n-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 - 1) + (n - 1) = m + n - 2步的,其中每一步都有可能向下,或者向右。其中向下的总步数为m - 1是确定的,只是具体是哪一步并不确定。所以问题就相当于总共有m + n - 2个不同的数,需要从中取m - 1个数,问总共有多少种取法?也就是求\(C_{m+n-2}^{m-1}\)。计算\(C_{a}^{b}\)需要令分子为a * (a - 1) * (a - 2) * ··· * (a - b + 1),项数为b,分母为1 * 2 * 3 * ··· * b,项数也为b,分子与分母相除就得到了答案。

        而在实践中如果先计算分母,那可能就会因为数值太大而溢出。而因为分母与分子的项数都是b,都需要进行b次循环,那么就可以在循环的时候同时计算分母和分子,并在每次计算时就将分母与分子相除,从而避免溢出。

class Solution {
public:
    int uniquePaths(int m, int n) {
        int times = m - 1;
        long long upBegin = m + n - 2;
        long long up = 1, down = m - 1;
        while(times--) {
            up *= upBegin--;
            while (down != 0 && up % down == 0) {
                up /= down;
                --down;
            }
        }
        return up;
    }
};

这种方法计算组合数的代码还是不容易理解的,可以多熟悉理解下当做以数论方式计算组合数的模板。

        二刷:忘记数论方法和它对应的计算组合数方法。


        第2题(LeetCode 63. 不同路径 II)相比第1题的不同只是增加了障碍物。所以状态转移矩阵方面相比上一题的改变就是,当遇到障碍物时,因为没有路径能走到障碍物上,所以将dp[i][j]的数值设置为0。初始化方面,因为在第0行一旦遇到一个障碍物,那它和它右边所有的点都是不可达的。第0列也同样如此,如果中途遇到一个障碍物,那它和它下边所有的点也都是不可达的。所以对于这些不可达的点,应该在初始化时就将其值设置为0,而不是原本的1。其他方面都与上一题一致。

        同样地,这道题也可以用一行数组来代替矩阵。需要对应额外做的是,在遍历每一行时,都判断下该行的首个位置是否是障碍物,如果是的话,就要将之后每一行dp数组的第0列也设置为0。具体的实践方法可以是设定一个flag。在每行都将flag值赋值给dp的首列元素。一旦遇到障碍物就将flag设置为零。这里容易疏忽一个点,就是flag的初始化。如果惯性思维将其初始化为1,那对于“第0行的第0个元素就是障碍物,但其他行的第0列元素并没有障碍物”的情况,由于是从第1行开始遍历的,那就会错误地将其他行的第0列元素设置为1。所以应该将flag初始化为“已经初始化好的dp的第0列”。

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

 在初始化部分,自己刚开始实现的方法是将dp数组全部初始化为1。然后遍历dp数组,在遇到障碍物的时候,再将其和之后的位置都设置为0。但可以使用相反的思维,使得代码更简洁。将dp数组全部初始化为0,然后遍历dp数组,不断地将当前值赋值为1,直到遇到障碍物为止。

        二刷:忘记节省空间写法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值