最大路徑和問題(摘櫻桃問題)

最大路徑和問題(摘櫻桃問題)

作者:Grey

原文地址: 最大路徑和問題(摘櫻桃問題)

題目鏈接

LeetCode 741. 摘櫻桃

主要思路

本題的難點在於嘗試,如何模擬一來一回的情況,我們可以這樣做,定義兩個小人,兩個人都從(0,0)位置出發,到右下角位置,每人同時選擇不同的下一步,如果兩個小人跳到了同一個位置,只計算一份。由於兩個小人是同時走,而且每次只走一步,所以,兩個小人一定是同時到達右下角的,兩個小人一路收集的櫻桃數量,就是一來一回收集的數量。

我們可以寫出第一個嘗試版本,定義遞歸函數

int p(int[][] matrix, int m, int n, int x1, int y1, int x2, int y2)

遞歸函數的含義表示,第一個小人從(x1,y1)開始到右下角,第二個小人從(x2,y2)開始到右下角,獲取到的最大值是多少。所以p(matrix, m, n, 0, 0, 0, 0)就是我們需要的答案。

接下來考慮base case,即:兩個小人都到達了右下角,由於題目已經說了,右下角的值一定不是-1,所以,可以獲得一份櫻桃數量。

        if (x1 == m - 1 && y1 == n - 1) {
            // 已經到了最右下角了
            // 隱含條件:另外一個點也一定到達右下角
            // 獲得一份櫻桃數
            return matrix[x1][y1];
        }

接下來就是普遍嘗試,分別可以分成如下四種情況:

情況1:第一個小人往下,第二個小人往右。

情況2:第一個小人往下,第二個小人往下。

情況3:第一個小人往右,第二個小人往下。

情況4:第一個小人往右,第二個小人往右。

但是在走上述任何分支的時候,記得要滿足兩個條件

第一個條件,不能越界

第二個條件,下一個要走的位置不能是-1,因爲題目說到,-1是不能走的位置。

        int next = -1;
        // 下,下
        if (x1 + 1 < m && x2 + 1 < m && matrix[x1 + 1][y1] != -1 && matrix[x2 + 1][y2] != -1) {
            next = Math.max(p(matrix, m, n, x1 + 1, y1, x2 + 1, y2), next);
        }
        // 下,右
        if (x1 + 1 < m && y2 + 1 < n && matrix[x1 + 1][y1] != -1 && matrix[x2][y2 + 1] != -1) {
            next = Math.max(p(matrix, m, n, x1 + 1, y1, x2, y2 + 1), next);
        }
        // 右,下
        if (y1 + 1 < n && x2 + 1 < m && matrix[x1][y1 + 1] != -1 && matrix[x2 + 1][y2] != -1) {
            next = Math.max(p(matrix, m, n, x1, y1 + 1, x2 + 1, y2), next);
        }
        // 右,右
        if (y1 + 1 < n && y2 + 1 < m && matrix[x1][y1 + 1] != -1 && matrix[x2][y2 + 1] != -1) {
            next = Math.max(p(matrix, m, n, x1, y1 + 1, x2, y2 + 1), next);
        }

經過上述四個分支,如果next的值還是-1,說明無路可走,返回-1

        if (next == -1) {
            return -1;
        }

如果next不等於-1,說明有可以走的路徑,那麼繼續判斷兩個小人是否在同一位置,如果在同一個位置,則只收集一份櫻桃,如果不在同一個位置,收集兩個小人所在位置的櫻桃之和。

        if (x1 == x2) {
            // 到達同一個位置,只取一個值
            return next + matrix[x1][y1];
        }
        return next + matrix[x1][y1] + matrix[x2][y2];

注:判斷同一個位置,只需要一個維度上的座標相等即可

完整代碼

    public static int cherryPickup1(int[][] matrix) {
        return Math.max(0, p(matrix, matrix.length, matrix[0].length, 0, 0, 0, 0));
    }

    // 定義兩個小人,兩個人都從(0,0)位置出發,到右下角位置,每人同時選擇不同的下一步,如果兩個小人跳到了同一個位置,只計算一份。
    public static int p(int[][] matrix, int m, int n, int x1, int y1, int x2, int y2) {
        if (x1 == m - 1 && y1 == n - 1) {
            // 已經到了最右下角了
            // 隱含條件:另外一個點也一定到達右下角
            // 獲得一份櫻桃數
            return matrix[x1][y1];
        }

        int next = -1;
        // 下,下
        if (x1 + 1 < m && x2 + 1 < m && matrix[x1 + 1][y1] != -1 && matrix[x2 + 1][y2] != -1) {
            next = Math.max(p(matrix, m, n, x1 + 1, y1, x2 + 1, y2), next);
        }
        // 下,右
        if (x1 + 1 < m && y2 + 1 < n && matrix[x1 + 1][y1] != -1 && matrix[x2][y2 + 1] != -1) {
            next = Math.max(p(matrix, m, n, x1 + 1, y1, x2, y2 + 1), next);
        }
        // 右,下
        if (y1 + 1 < n && x2 + 1 < m && matrix[x1][y1 + 1] != -1 && matrix[x2 + 1][y2] != -1) {
            next = Math.max(p(matrix, m, n, x1, y1 + 1, x2 + 1, y2), next);
        }
        // 右,右
        if (y1 + 1 < n && y2 + 1 < m && matrix[x1][y1 + 1] != -1 && matrix[x2][y2 + 1] != -1) {
            next = Math.max(p(matrix, m, n, x1, y1 + 1, x2, y2 + 1), next);
        }
        if (next == -1) {
            return -1;
        }
        if (x1 == x2) {
            // 到達同一個位置,只取一個值
            return next + matrix[x1][y1];
        }
        return next + matrix[x1][y1] + matrix[x2][y2];
    }

這個解法在 LeetCode 上直接超時。

在上述嘗試的基礎上,我們可以做進一步的優化,遞歸函數現在是四個可變參數,根據我們的設計,其實可以得到如下公式

x1 + y1 = x2 + y2

那麼我們可以將遞歸函數省略一個參數y2,因爲

y2 = x1 + y1 - x2

上述遞歸方法我們可以修改爲

// 省略y2參數
    public static int p(int[][] matrix, int m, int n, int x1, int y1, int x2) {
        if (x1 == m - 1 && y1 == n - 1) {
            // 已經到了最右下角了
            // 隱含條件:另外一個點也一定到達右下角
            // 獲得一份櫻桃數
            return matrix[x1][y1];
        }

        int next = -1;
        // 下,下
        if (x1 + 1 < m && x2 + 1 < m && matrix[x1 + 1][y1] != -1 && matrix[x2 + 1][getY2(x1, y1, x2)] != -1) {
            next = Math.max(p(matrix, m, n, x1 + 1, y1, x2 + 1), next);
        }
        // 下,右
        if (x1 + 1 < m && getY2(x1, y1, x2) + 1 < n && matrix[x1 + 1][y1] != -1 && matrix[x2][getY2(x1, y1, x2) + 1] != -1) {
            next = Math.max(p(matrix, m, n, x1 + 1, y1, x2), next);
        }
        // 右,下
        if (y1 + 1 < n && x2 + 1 < m && matrix[x1][y1 + 1] != -1 && matrix[x2 + 1][getY2(x1, y1, x2)] != -1) {
            next = Math.max(p(matrix, m, n, x1, y1 + 1, x2 + 1), next);
        }
        // 右,右
        if (y1 + 1 < n && getY2(x1, y1, x2) + 1 < m && matrix[x1][y1 + 1] != -1 && matrix[x2][getY2(x1, y1, x2) + 1] != -1) {
            next = Math.max(p(matrix, m, n, x1, y1 + 1, x2), next);
        }
        if (next == -1) {
            return -1;
        }
        if (x1 == x2) {
            // 到達同一個位置,只取一個值
            return next + matrix[x1][y1];
        }
        return next + matrix[x1][y1] + matrix[x2][getY2(x1, y1, x2)];
    }
    public static int getY2(int x1, int y1, int x2) {
        return x1 + y1 - x2;
    }

我們可以做進一步的優化,由於三個可變參數的範圍分別是[0...m-1][0...n-1][0...m-1],那麼我們可以設置三維dp,將所有遞歸結果緩存起來

int[][][] dp = new int[m][n][m];

三維dp的所有位置初始值設置爲Integer.MIN_VALUE,在遞歸方法中,將這個三維表作爲緩存帶入參數中,每次遞歸過程中,先從dp表中拿值,如果值不爲Integer.MIN_VALUE,說明這個過程算過,直接取值即可。

    public static int p(int[][] matrix, int m, int n, int x1, int y1, int x2, int[][][] dp) {
        if (dp[x1][y1][x2] != Integer.MIN_VALUE) {
            // 說明這個過程曾經算過,直接從緩存中取值即可
            return dp[x1][y1][x2];
        }
    }

然後在每次遞歸過程中,將答案存入緩存,完整代碼如下

    // 動態規劃
    // 三維表
    public static int cherryPickup(int[][] matrix) {
        int m = matrix.length;
        int n = matrix[0].length;
        int[][][] dp = new int[m][n][m];
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                for (int k = 0; k < m; k++) {
                    // 初始值設置爲Integer.MIN_VALUE,只要不等於Integer.MIN_VALUE,就說明遞歸過程已經算過了
                    dp[i][j][k] = Integer.MIN_VALUE;
                }
            }
        }
        return Math.max(0, p(matrix, m, n, 0, 0, 0, dp));
    }

    // 定義兩個小人,兩個人都從(0,0)位置出發,到右下角位置,每人同時選擇不同的下一步,如果兩個小人跳到了同一個位置,只計算一份。
    public static int p(int[][] matrix, int m, int n, int x1, int y1, int x2, int[][][] dp) {
        if (dp[x1][y1][x2] != Integer.MIN_VALUE) {
            // 說明這個過程曾經算過,直接從緩存中取值即可
            return dp[x1][y1][x2];
        }
        if (x1 == m - 1 && y1 == n - 1) {
            // 已經到了最右下角了
            // 隱含條件:另外一個點也一定到達右下角
            // 獲得一份櫻桃數
            dp[x1][y1][x2] = matrix[x1][y1];
            return matrix[x1][y1];
        }

        int next = -1;
        // 下,下
        if (x1 + 1 < m && x2 + 1 < m && matrix[x1 + 1][y1] != -1 && matrix[x2 + 1][getY2(x1, y1, x2)] != -1) {
            next = Math.max(p(matrix, m, n, x1 + 1, y1, x2 + 1, dp), next);
        }
        // 下,右
        if (x1 + 1 < m && getY2(x1, y1, x2) + 1 < n && matrix[x1 + 1][y1] != -1 && matrix[x2][getY2(x1, y1, x2) + 1] != -1) {
            next = Math.max(p(matrix, m, n, x1 + 1, y1, x2, dp), next);
        }
        // 右,下
        if (y1 + 1 < n && x2 + 1 < m && matrix[x1][y1 + 1] != -1 && matrix[x2 + 1][getY2(x1, y1, x2)] != -1) {
            next = Math.max(p(matrix, m, n, x1, y1 + 1, x2 + 1, dp), next);
        }
        // 右,右
        if (y1 + 1 < n && getY2(x1, y1, x2) + 1 < m && matrix[x1][y1 + 1] != -1 && matrix[x2][getY2(x1, y1, x2) + 1] != -1) {
            next = Math.max(p(matrix, m, n, x1, y1 + 1, x2, dp), next);
        }
        if (next == -1) {
            dp[x1][y1][x2] = -1;
            return -1;
        }
        if (x1 == x2) {
            // 到達同一個位置,只取一個值
            // 將答案存入緩存
            dp[x1][y1][x2] = next + matrix[x1][y1];
            return dp[x1][y1][x2];
        }
        // 將答案存入緩存
        dp[x1][y1][x2] = next + matrix[x1][y1] + matrix[x2][getY2(x1, y1, x2)];
        return dp[x1][y1][x2];
    }

    public static int getY2(int x1, int y1, int x2) {
        return x1 + y1 - x2;
    }

時間複雜度O(m*n*m),LeetCode 中直接AC。

更多

算法和數據結構筆記

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