leetcode62. Unique Paths(C++从暴力递归到动态规划)

这道题也是老生常谈,非常经典的一道题目了,围绕着此题能学到各种各样的算法思想。本文主要梳理从暴力递归到动态规划、再到数学炫技的求解过程演变。
题目如下:
一个机器人位于 m , n m, n m,n网格左上角,它每次只能向右或者向下移动一格,问它到达图中右下角有多少种不同的路径?
在这里插入图片描述

暴力递归

首先很容易想到的就是递归写法,枚举并统计所有可以到达右下角的路径。画一下递归树,如下图所示,算法复杂度为 O ( 2 m n ) O(2^{mn}) O(2mn),空间复杂度为O(mn),因为创建了二维数组。
在这里插入图片描述

代码如下

int ans;
    int m, n;
    int dx[3] = {
    
    0, 1, 0};
    vector<vector<int>> grid;
    void dfs(int x, int y){
    
    
        if(x == m-1 && y == n-1){
    
    
            ans++;
            return;
        }
        for(int i = 0;i < 2;i++){
    
    
            int nx = x + dx[i];
            int ny = y + dx[i+1];
            if(nx >= m || ny >= n || nx < 0 || ny < 0 || grid[nx][ny] != 0) continue;
            grid[nx][ny] = 1;
            dfs(nx, ny);
            grid[nx][ny] = 0;
        }
    }
    int uniquePaths(int m, int n) {
    
    
        ans = 0;
        this->m = m;
        this->n = n;
        grid = vector<vector<int>>(m, vector<int>(n, 0));
        grid[0][0] = 1;
        dfs(0, 0);
        return ans;
    }

不过这样的代码直接提交会报TLE超时,因为在递归的过程中极易出现重复计算。例如下图所示,在计算到达C的所有情况总数时,在dfs中会把到达B的情况数重新算一遍。
在这里插入图片描述
所以想优化就需要对递归树进行减支,那么较为简单的做法就是用一个dict来存已经计算过的路径总数啦。写出的代码如下图

int ans;
    int m, n;
    int dx[3] = {
    
    0, 1, 0};
    vector<vector<int>> grid;
    vector<vector<int>> memo; // 记录算过的路径情况数
    int dfs(int x, int y){
    
    
        int res = 0;
        if(x == m-1 && y == n-1) return 1;
        if(memo[x][y] != -1) return memo[x][y];
        // int tmp = grid[x][y];
        for(int i = 0;i < 2;i++){
    
    
            int nx = x + dx[i];
            int ny = y + dx[i+1];
            if(nx >= m || ny >= n || nx < 0 || ny < 0 || grid[nx][ny] != 0) continue;
            grid[nx][ny] = 1;
            res += dfs(nx, ny);
            grid[nx][ny] = 0;
        }
        return memo[x][y] = res;
    }
    int uniquePaths(int m, int n) {
    
    
        ans = 0;
        this->m = m;
        this->n = n;
        grid = vector<vector<int>>(m, vector<int>(n, 0));
        memo = vector<vector<int>>(m, vector<int>(n, -1));
        grid[0][0] = 1;
        return dfs(0, 0);
    }

这种解法有个高端的名字,叫做记忆化递归。顾名思义,就是在递归过程中记录已经求过的解,来达到优化的目的。这样的解就能通过leetcode的刁难了。可对于这个题,它还能进一步优化,那就要上老生常谈的动态规划了。动态规划解法是这样考虑问题的,对于每一个位置 ( i , j ) (i, j) (i,j),到达该位置的路径总数恰好等于到达 ( i , j − 1 ) (i, j-1) (i,j1)的情况总数加上 ( i − 1 , j ) (i-1, j) (i1,j)的情况总数。这是由题目的条件所决定的,每次只能向右或者向下移动。那我们就找到当前位置和上一次位置的解有哪些练习,就能够想到这样的递推式了。对于第一行和第一列,显然到达这些位置的路径只有一条,这就是dp的初始条件。于是自然而然的得到以下的实现

int uniquePaths(int m, int n) {
    
    
        vector<vector<int>> dp(m, vector<int>(n, 0));
        for(int i = 0;i < n;i++){
    
    
            dp[0][i] = 1;
        }
        for(int i = 0;i < m;i++){
    
    
            dp[i][0] = 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(mn),空间复杂度也为O(mn),但比暴力递归不知道强了多少倍。当然这样的解法还可以优化,由于dp的状态转移过程仅从 ( i , j ) (i,j) (i,j)的左侧或者上方而来,因此只需要每次存储更新这两个位置的值就可以。可以简化成如下时间复杂度不变,空间复杂度转为O(n)的解法

int uniquePaths(int m, int n) {
    
    
        int left = 1;
        int cur;
        if(m == 1) return 1;
        vector<int> top(n, 0); // 表示列
        for(int i = 0;i < n;i++){
    
    
            top[i] = 1;
        }
        for(int i = 1;i < m;i++){
    
    
            left = 1;
            for(int j = 1;j < n;j++){
    
    
                cur = left + top[j];
                left = cur;
                top[j] = cur;
            }
        }
        return cur;
    }

最后再提供一种网上看到的比较优秀的数学解法。由于从Start到Finish肯定要横着向右走n-1次,再竖着向下走m-1次,一共要走m+n-2步。那么到达 ( m − 1 , n − 1 ) (m-1, n-1) (m1,n1)的解等于 C m + n − 2 m − 1 C m − 1 m − 1 C_{m+n-2}^{m-1}C_{m-1}^{m-1} Cm+n2m1Cm1m1 C m + n − 2 n − 1 C n − 1 n − 1 C_{m+n-2}^{n-1}C_{n-1}^{n-1} Cm+n2n1Cn1n1,这两个求解的结果肯定是一样的的。相当于从总数不变的一共m+n-2步中,先选出竖着走/横着走的可能情况,再走剩下的。代码如下

int uniquePaths(int m, int n) {
    
    
    int N = n + m - 2;
    double res = 1;
    for (int i = 1; i < m; i++)
        res = res * (N - (m - 1) + i) / i;
    return int(res);
}

相比于上面的做法更加简短,但每个数的阶乘和组合数都可以进行预先计算,在频繁求解的场景下,要比动态规划算法更快。

猜你喜欢

转载自blog.csdn.net/weixin_42474261/article/details/121458036