算法-動態規劃-最小路徑和

算法-動態規劃-最小路徑和

1 題目概述

1.1 題目出處

https://leetcode-cn.com/problems/minimum-path-sum/

1.2 題目描述

給定一個無序的整數數組,找到其中最長上升子序列的長度。

示例:

輸入: [10,9,2,5,3,7,101,18]
輸出: 4
解釋: 最長的上升子序列是 [2,3,7,101],它的長度是 4。
說明:

可能會有多種最長上升子序列的組合,你只需要輸出對應的長度即可。
你算法的時間複雜度應該爲 O(n2) 。

進階: 你能將算法的時間複雜度降低到 O(n log n) 嗎?

2 BFS

2.1 思路

從左上角開始,進行BFS,每次拿出隊首元素,然後分別計算該元素的路徑和加上右側和下側元素的數值之和,看是否分別小於當前右和下側元素的路徑和,如果小於就更新,並且在這些元素未訪問過時推入隊列末尾進行BFS。

2.2 代碼

class Solution {
    public int minPathSum(int[][] grid) {
        int result = 0;
        if(grid == null || grid.length == 0){
            return result;
        }

        int m = grid.length - 1;
        int n = grid[0].length - 1;

        // 用來存放某節點的最小路徑和
        int[][] record = new int[m + 1][n + 1];  
        for(int i = 0; i < record.length; i++){
            for(int j = 0; j < record[0].length; j++){
                record[i][j] = -1;
            }
        }
        
       
        // 思路 從左上開始,分別計算右側和下側節點的最小路徑和,並推入隊列等待計算

        LinkedList<int[]> queue = new LinkedList<>();
        record[0][0] = grid[0][0];
        int[] first = {0, 0, record[0][0]};
        queue.add(first);
        while(!queue.isEmpty()){
            int[] ele = queue.poll();
            if(ele[1] < n){
                if(record[ele[0]][ele[1] + 1] == -1){
                    record[ele[0]][ele[1] + 1] = record[ele[0]][ele[1]] + grid[ele[0]][ele[1] + 1];
                    int[] right = {ele[0], ele[1] + 1, record[ele[0]][ele[1] + 1]};
                    queue.add(right);
                }else{
                    record[ele[0]][ele[1] + 1] = Math.min(record[ele[0]][ele[1] + 1], record[ele[0]][ele[1]] + grid[ele[0]][ele[1] + 1]);
                }
            }

            if(ele[0] < m){
                if(record[ele[0] + 1][ele[1]] == -1){
                    record[ele[0] + 1][ele[1]] = record[ele[0]][ele[1]] + grid[ele[0] + 1][ele[1]];
                    int[] below = {ele[0] + 1, ele[1], record[ele[0] + 1][ele[1]]};
                    queue.add(below);
                }else{
                    record[ele[0] + 1][ele[1]] = Math.min(record[ele[0] + 1][ele[1]], record[ele[0]][ele[1]] + grid[ele[0] + 1][ele[1]]);
                }
            }
        }
        return record[m][n];
    }
}

2.3 時間複雜度

在這裏插入圖片描述
O(m*n)

2.4 空間複雜度

O(m*n)

3 DFS

3.1 思路

既然寫了BFS,那爲了練習我們可以來個DFS。

但有個問題,如果採用DFS,先遍歷右側,那可能導致有些節點因爲已經被訪問過而沒有更新更小的值!

也就是說,我們不能像之前那樣不管已經訪問過的節點,需要再次訪問!

3.2 代碼

class Solution {
    private int m = 0;

    private int n = 0;
    public int minPathSum(int[][] grid) {
        int result = 0;
        if(grid == null || grid.length == 0){
            return result;
        }

        m = grid.length - 1;
        n = grid[0].length - 1;

        // 用來存放某節點的最小路徑和
        int[][] record = new int[m + 1][n + 1];  
        for(int i = 0; i < record.length; i++){
            for(int j = 0; j < record[0].length; j++){
                record[i][j] = -1;
            }
        }

        record[0][0] = grid[0][0];
        dfs(grid, record, 0, 0);
        return record[m][n];
    }

    private void dfs(int[][] grid, int[][] record, int i, int j){
        if(j < n){
            if(record[i][j + 1] == -1){
                record[i][j + 1] = record[i][j] + grid[i][j + 1];
                
            }else{
                record[i][j + 1] = Math.min(record[i][j + 1], record[i][j] + grid[i][j + 1]);
            }
            dfs(grid, record, i, j+1);
        }

        if(i < m){
            if(record[i + 1][j] == -1){
                record[i + 1][j] = record[i][j] + grid[i + 1][j];
                
            }else{
                record[i + 1][j] = Math.min(record[i + 1][j], record[i][j] + grid[i + 1][j]);
            }
            dfs(grid, record, i+1, j);
        }
    }
}

3.3 時間複雜度

在這裏插入圖片描述
果然,超出了時間限制!

4 DFS-優化

4.1 思路

從右下往左上進行DFS:

record[i][j] = grid[i][j] + Math.min(dfs(grid, record, i - 1, j), dfs(grid, record, i, j - 1));

還是採用record進行記錄,但注意這裏因爲已經是最終的最小路徑和,所以重複訪問時可以直接返回該值,不需要再進行計算!

4.2 代碼

class Solution {
    private int m = 0;

    private int n = 0;
    public int minPathSum(int[][] grid) {
        int result = 0;
        if(grid == null || grid.length == 0){
            return result;
        }

        m = grid.length - 1;
        n = grid[0].length - 1;
        // 1.緩存最小路徑和
        // 2.標識是否訪問過
        Integer[][] record = new Integer[m + 1][n + 1];

        // 思路 從右下開始往左和上推
        return dfs(grid, record, m, n);
    }

    private int dfs(int[][] grid, Integer[][] record, int i, int j){
        if(i < 0 || j < 0){
            // 越界
            return Integer.MAX_VALUE;
        }
        
        if(i == 0 && j == 0){
            return grid[0][0];
        }

        if(record[i][j] != null){
            return record[i][j];
        }
        record[i][j] = grid[i][j] + Math.min(dfs(grid, record, i - 1, j), dfs(grid, record, i, j - 1));
        return record[i][j];
    }
}

4.3 時間複雜度

在這裏插入圖片描述
O(m*n)

4.4 空間複雜度

O(m*n)

5 動態規劃1

5.1 思路

前面BFS看起來不錯,但其實好像沒有必要用隊列,而且速度也很慢。我們改成動態規劃試試。

因爲我們遍歷採用的是從左到右,從上到下,所以是可以保證每個節點都是最小路徑和。

動態規劃轉移方程如下:

dp[i][j] = Math.min(dp[i - 1][j], dp[i][j-1]) + grid[i-1][j-1];

其中dp[i][j]表示該節點的最小路徑和。

5.2 代碼

class Solution {
    public int minPathSum(int[][] grid) {
        int result = 0;
        if(grid == null || grid.length == 0){
            return result;
        }

        int m = grid.length;
        int n = grid[0].length;

        // 用來存放某節點的最小路徑和
        int[][] dp = new int[m + 1][n + 1];  
        // 在外圍虛擬一圈,除了grid[0][0]的左和上爲0外,其他值都爲Integer.MAX_VALUE;
        // 這樣做的目的是爲了動態規劃遍歷中不用管邊界值
        int[] zeroRow = dp[0];
        for(int i = 2; i < zeroRow.length; i++){
            zeroRow[i] = Integer.MAX_VALUE;
        }
        for(int i = 2; i < m + 1; i++){
            dp[i][0] = Integer.MAX_VALUE;
        }

        for(int i = 1; i <= m; i++){
            for(int j = 1; j <= n; j++){
                dp[i][j] = Math.min(dp[i - 1][j], dp[i][j-1]) + grid[i-1][j-1];
            }
        }  
        return dp[m][n];
    }
}

5.3 時間複雜度

在這裏插入圖片描述
O(m*n)

這次速度快很多了!

5.4 空間複雜度

O(m*n)

6 動態規劃2

6.1 思路

前面動態規劃1看起來很妙,但還用了額外空間。

仔細觀察,其實按上面方法遍歷,每個元素的初始值只用了一次,那麼可以複用原始數組grid,遍歷過程中將計算的最小路徑和放入裏面。也就是說grid[i][j]最終表示該節點的最小路徑和。

這樣,最後直接返回grid[m-1][n-1]即可

6.2 代碼

class Solution {
    public int minPathSum(int[][] grid) {
        int result = 0;
        if(grid == null || grid.length == 0){
            return result;
        }

        int m = grid.length;
        int n = grid[0].length;

        for(int i = 0; i < m; i++){
            for(int j = 0; j < n; j++){
                if(i + j == 0){
                    continue;
                }else if (i == 0){
                    grid[i][j] = grid[i][j-1] + grid[i][j];
                }else if (j == 0){
                    grid[i][j] = grid[i - 1][j] + grid[i][j];
                }else{
                    grid[i][j] = Math.min(grid[i - 1][j], grid[i][j-1]) + grid[i][j];
                }
            }
        }  
        return grid[m-1][n-1];
    }
}

6.3 時間複雜度

O(m*n)

6.4 空間複雜度

在這裏插入圖片描述
O(1)
因爲複用原數組grid,沒有額外空間。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章