leetcode62. Unique Paths (C++ from violent recursion to dynamic programming)

This question is also a cliché and a very classic question. You can learn a variety of algorithmic ideas around this question. This article mainly summarizes the evolution of the solution process from violent recursion to dynamic programming, and then to mathematical dazzling skills.
The topic is as follows:
A robot is located at m, n m, n m,nThe upper left corner of the grid, it can only move one square to the right or down at a time. How many different paths can it take to reach the lower right corner of the picture?
Insert image description here

violent recursion

The first thing that easily comes to mind is the recursive writing method, enumerates and counts all the paths that can reach the lower right corner. Draw a recursion tree, as shown in the figure below. The algorithm complexity is O ( 2 m n ) O(2^{mn}) O(2mn), the space complexity is O(mn) because a two-dimensional array.
Insert image description here

code show as below

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;
    }

However, if such code is submitted directly, a TLE timeout will be reported, because double calculations are easy to occur during the recursive process. For example, as shown in the figure below, when calculating the total number of all situations that reach C, the number of situations that reach B will be recalculated in dfs.
Insert image description here
Therefore, if you want to optimize, you need to reduce the branches of the recursive tree. The simpler way is to use a dict to store the total number of paths that have been calculated. The code written is as shown below

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);
    }

This solution has a high-end name, calledMemory Recursion. As the name suggests, it records the solutions that have been found during the recursive process to achieve the purpose of optimization. Such a solution can pass leetcode's difficulties. But for this problem, it can be further optimized, so we have to use the cliché dynamic programming. The dynamic programming solution considers the problem like this, for each position (i, j) (i, j) (i,j), the total number of paths to reach this position is exactly equal to ( i , j − 1 ) (i, j-1) (i,j1)'s situation addition ( i − 1 , j ) (i-1 , j) (i1,The total number of cases of j). This is determined by the conditions of the question. It can only move to the right or down each time. Then we can find out what exercises are available for the solutions to the current position and the previous position, and we can think of such a recursive formula. For the first row and column, there is obviously only one path to reach these positions, which is the initial condition of dp. So the following realization is naturally obtained

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];
    }

Isn’t it a lot more refreshing all of a sudden? The complexity of the algorithm solved in this way is O(mn), and the space complexity is also O(mn), but it is many times better than violent recursion. Of course, such a solution can also be optimized, because the state transition process of dp only starts from ( i , j ) (i,j) (i,j) from the left or above, so you only need to store and update the values ​​​​of these two locations each time. It can be simplified to the following solution where the time complexity remains unchanged and the space complexity is converted to 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;
    }

Finally, I will provide an excellent mathematical solution found on the Internet. Since from Start to Finish, you must walk to the right sideways n-1 times, and then walk down vertically m-1 times, a total of m+n-2 steps. Then arrive ( m − 1 , n − 1 ) (m-1, n-1) (m1,n1)'s solution, etc. 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, the results of these two solutions must be the same. It is equivalent to selecting the possible situation of walking vertically/horizontally from a total of m+n-2 steps with the same total number, and then taking the remaining ones. code show as below

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);
}

Compared with the above method, it is simpler, but the factorial and combination number of each number can be pre-calculated, which is faster than the dynamic programming algorithm in frequent solution scenarios.

Guess you like

Origin blog.csdn.net/weixin_42474261/article/details/121458036