「アルゴリズムシリーズ」の動的計画法

導入

  面接官がよく受ける試験にはさまざまな種類があり、面接官によって質問も異なりますが、ほとんどの面接官が好む質問があるとすれば、それはアルゴリズム質問の無冠の王である動的計画法に違いありませなぜそんなことを言うのですか?変動性が大きいため、主題の法則を見つけるのは難しく、ほとんどの場合でも明確ではないため、この問題を解決するには動的計画法を使用する必要があります。同様の質問をしたことがない限り、次の動的計画法を必ず行うとは言い切れません。新たな問題が生じたとき、私たちは依然として無力かもしれません。さらに、インターネット上の多くのコンテンツにより、ただでさえ複雑なものがさらに複雑になりました。今日引き算をするとき、覚えておく必要があるのは 1 つの文だけです:問題解決のステップ + 状態遷移方程式 = 動的計画法の答え

理論的根拠

  動的プログラミング(英語: Dynamic programming、DP と呼ばれる)は、元の問題を比較的単純な部分問題に分解することによって複雑な問題を解決するために、数学経営科学コンピューターサイエンス経済学、生物情報学で使用される手法です動的プログラミングは、多くの場合、重複する部分問題と最適な部分構造のプロパティを持つ問題に適用でき、すべての部分問題の結果を記録するため、動的プログラミング手法は多くの場合、単純な解決法よりもはるかに短時間で済みます   動的プログラミングには、ボトムアップトップダウンという 2 つの問題解決方法があります。トップダウンはメモ化された再帰ボトムアップは再帰です動的計画法を使用して解かれた問題には明らかな特徴があります。部分問題は一度解かれると、その後の計算プロセスでは変更されません。この特徴は残効なしと呼ばれます。問題を解くプロセスは有向非巡回グラフを形成します動的計画法は各部分問題を一度ずつ解くだけで、自然な枝刈りの機能があり、計算量を削減します。
  

問題解決のステップ

  動的計画法には、問題に対する決まった答えはありません。それぞれの問題は異なる状態遷移方程式ですが、問題解決の手順を要約すると、動的計画法の困難な問題を大幅に解決できます。問題解決のステップと状態遷移方程式は、すべての動的プログラミング問題のテンプレートです。一般的な問題解決の手順には、次の 3 つの手順が必要です。

  1. dp テーブル (dp テーブル) と添字の意味を確認します
    。動的ルールでは、各変更の記憶媒体として配列が必要です。場合によっては、実際に配列の役割を果たす 2 つの変数のみを使用することもできます。そして、配列の全体的な意味各添字の意味が何を表すのかを明確に知る必要があります。配列の全体的な意味とは、何を保存するのかということです。添字の意味は、各ステップで保存する値の意味ですそれが理解できて初めて、問題が終わった後に混乱することはなくなり、次に問題を行うときは、自分の感覚に頼らなければなりません。書かないなら書いてください。
  2. 漸化式の決定、つまり状態遷移方程式の決定
    状態遷移方程式は運動規則問題を解く核心です運動規則は元の問題を多くの小さなステップに分解して解くものだと言いませんでしたか?小さな一歩はどう変わるのか?あるいは、それぞれのステップをどのようにとるべきでしょうか? 実は、各ステップ間の変化関係を記述しているのがこの状態遷移方程式であり、例えば i から i + 1 への変化関数が状態遷移方程式です
  3. dp 配列の初期化方法とトラバース方向を決定する問題解決の手順全体で、dp 配列の初期化トラバース方向の詳細
    を無視しがちですが、実際には同じように重要です。例えば、初期化するときに0から始めるのと1から始めるのでは意味が異なり、最終的な結果も異なりますもう 1 つの例は、トラバース方向です。常に左から右に進むわけではありません。2 次元配列に関しては、上から下、右から左に進むこともあります

他の

  動的計画法には、さらに難しい種類の問題、つまりナップザック問題があり、この大きな牛の記事「バックパックに関する九つの講義」を読むことができます。その中でも、 01 ナップサック問題完全ナップサック問題に焦点を当てることができますので、機会があれば関連する内容を共有します。

問題解決の経験

  • 動的プログラミングは非常に変化しやすいため、問題の意味を理解するにはさらに練習が必要です。
  • 動的質問の配列の意味を判断することは、質問を解くための鍵の 1 つです。
  • 状態遷移方程式は動的規則問題の核心であり、書き出すと一撃で仕留められる。
  • dp 配列がどのように初期化されるかは、走査方向と同じくらい重要であり、無視することはできません。
  • 動的計画法には固定の問題解決テンプレートはありませんが、統一された問題解決ステップがあり、問題解決ステップと状態遷移方程式がすべての動的計画問題のテンプレートとなります。
  • 動的プログラミングと貪欲アルゴリズムの違いは、貪欲アルゴリズムには状態導出がなく、ローカルから最良のものを直接選択すること、および動的計画ではグローバル情報を考慮する必要があることです。
  • ナップサック問題は動的ルールの発展型問題として使用できるので、01 ナップサック問題と完全ナップサック問題を中心に学習してください。

アルゴリズムのトピック

5. 最長の回文部分文字列

ここに画像の説明を挿入
トピック分析: 動的プログラミングの手順に従って問題を解決します。
コードは以下のように表示されます。

/**
 * 动态规划
 */
class Solution {
    public static String longestPalindrome(String s) {
    // 边界条件判断
    if (s.length() < 2)
        return s;
    // start表示最长回文串开始的位置,
    // maxLen表示最长回文串的长度
    int start = 0, maxLen = 1;
    int length = s.length();
    boolean[][] dp = new boolean[length][length];
    for (int right = 1; right < length; right++) {
        for (int left = 0; left < right; left++) {
            // 如果两种字符不相同,肯定不能构成回文子串
            if (s.charAt(left) != s.charAt(right))
                continue;

            // 下面是s.charAt(left)和s.charAt(right)两个
            // 字符相同情况下的判断
            // 如果只有一个字符,肯定是回文子串
            if (right == left) {
                dp[left][right] = true;
            } else if (right - left <= 2) {
                // 类似于"aa"和"aba",也是回文子串
                dp[left][right] = true;
            } else {
                // 类似于"a******a",要判断他是否是回文子串,只需要
                // 判断"******"是否是回文子串即可
                dp[left][right] = dp[left + 1][right - 1];
            }
            // 如果字符串从left到right是回文子串,只需要保存最长的即可
            if (dp[left][right] && right - left + 1 > maxLen) {
                maxLen = right - left + 1;
                start = left;
            }
        }
    }
    // 截取最长的回文子串
    return s.substring(start, start + maxLen);
    }
}

10. 正規表現マッチング

ここに画像の説明を挿入
トピック分析: 動的プログラミングの手順に従って問題を解決します。
コードは以下のように表示されます。

/**
 * 动态规划
 */
class Solution {
    public boolean isMatch(String s, String p) {
        int m = s.length();
        int n = p.length();

        boolean[][] f = new boolean[m + 1][n + 1];
        f[0][0] = true;
        for (int i = 0; i <= m; ++i) {
            for (int j = 1; j <= n; ++j) {
                if (p.charAt(j - 1) == '*') {
                    f[i][j] = f[i][j - 2];
                    if (matches(s, p, i, j - 1)) {
                        f[i][j] = f[i][j] || f[i - 1][j];
                    }
                } else {
                    if (matches(s, p, i, j)) {
                        f[i][j] = f[i - 1][j - 1];
                    }
                }
            }
        }
        return f[m][n];
    }

    public boolean matches(String s, String p, int i, int j) {
        if (i == 0) {
            return false;
        }
        if (p.charAt(j - 1) == '.') {
            return true;
        }
        return s.charAt(i - 1) == p.charAt(j - 1);
    }
}

42. 雨水を受ける

ここに画像の説明を挿入
トピック分析: 動的プログラミングの手順に従って問題を解決します。
コードは以下のように表示されます。

/**
 * 动态规划
 */
class Solution {
    public int trap(int[] height) {
        int n = height.length;
        if (n == 0) {
            return 0;
        }

        int[] leftMax = new int[n];
        leftMax[0] = height[0];
        for (int i = 1; i < n; ++i) {
            leftMax[i] = Math.max(leftMax[i - 1], height[i]);
        }

        int[] rightMax = new int[n];
        rightMax[n - 1] = height[n - 1];
        for (int i = n - 2; i >= 0; --i) {
            rightMax[i] = Math.max(rightMax[i + 1], height[i]);
        }

        int ans = 0;
        for (int i = 0; i < n; ++i) {
            ans += Math.min(leftMax[i], rightMax[i]) - height[i];
        }
        return ans;
    }
}

44. ワイルドカードマッチング

ここに画像の説明を挿入
トピック分析: 動的プログラミングの手順に従って問題を解決します。
コードは以下のように表示されます。

/**
 * 动态规划
 */
class Solution {
    public boolean isMatch(String s, String p) {
        int m = s.length();
        int n = p.length();
        boolean[][] dp = new boolean[m + 1][n + 1];
        dp[0][0] = true;
        for (int i = 1; i <= n; ++i) {
            if (p.charAt(i - 1) == '*') {
                dp[0][i] = true;
            } else {
                break;
            }
        }
        for (int i = 1; i <= m; ++i) {
            for (int j = 1; j <= n; ++j) {
                if (p.charAt(j - 1) == '*') {
                    dp[i][j] = dp[i][j - 1] || dp[i - 1][j];
                } else if (p.charAt(j - 1) == '?' || s.charAt(i - 1) == p.charAt(j - 1)) {
                    dp[i][j] = dp[i - 1][j - 1];
                }
            }
        }
        return dp[m][n];
    }
}

53. 最大サブアレイ合計

ここに画像の説明を挿入
トピック分析: 現在の数値を右境界として最大の数値を見つけ、そこから最大の数値を見つけます。
コードは以下のように表示されます。

/**
 * 动态规划
 */
class Solution {
    public int maxSubArray(int[] nums) {
        int res = nums[0];
        int max = nums[0];
        // 找出以当前数为右边界的最大数max,再从中找出res
        for (int i = 1; i < nums.length; i++) {
            if (max >= 0) {
                max += nums[i];
            } else {
                max = nums[i];
            }
            res = Math.max(res, max);
        }
        return res;
    }
}

62. 異なる道

ここに画像の説明を挿入
トピック分析: 動的プログラミングの手順に従って問題を解決します。毎回計算するのではなく、空間と時間を交換するキャッシュが必要になります。
コードは以下のように表示されます。

/**
 * 动态规划
 */
class Solution {
    public int uniquePaths(int m, int n) {
        // 申请内存用于缓存子路径结果,不用每次都计算,提高算法效率
        int[][] nums = new int[m][n];
        return helper(nums, m - 1, n - 1);
    }

    public int helper(int[][] nums, int row, int column) {
        int res = 0;
        // 递归出口
        if (row == 0 && column == 0) {
            res = 1;
        }
        if (row > 0 && column == 0) {
            // 判断是否在缓存中
            if (nums[row - 1][column] != 0) {
                res = nums[row - 1][column];
            } else {
                res = helper(nums, row - 1, column);
                // 将结果缓存
                nums[row][column] = res;
            }
        }
        if (row == 0 && column > 0) {
            // 判断是否在缓存中
            if (nums[row][column - 1] != 0) {
                res = nums[row][column - 1];
            } else {
                res = helper(nums, row, column - 1);
                // 将结果缓存
                nums[row][column] = res;
            }
        }
        if (row > 0 && column > 0) {
            // 判断是否在缓存中
            if (nums[row - 1][column] != 0 && nums[row][column - 1] != 0) {
                res = nums[row - 1][column] + nums[row][column - 1];
            } else {
                res = helper(nums, row - 1, column) + helper(nums, row, column - 1);
                // 将结果缓存
                nums[row][column] = res;
            }
        }
        return res;
    }
}

63. 異なる道 II

ここに画像の説明を挿入
トピック分析: 動的プログラミングの手順に従って問題を解決できます。この種の問題では、通常、バックトラッキングを使用してすべてのパスを見つけ、動的ルールを使用してすべての数値を見つけます。2 次元配列を実行できます。 。
コードは以下のように表示されます。

/**
 * 动态规划
 */
class Solution {
    public int uniquePathsWithObstacles(int[][] obstacleGrid) {
        int row = obstacleGrid.length;
        int column = obstacleGrid[0].length;
        // 记录所有数量的数组
        int[][] nums = new int[row][column];
        nums[0][0] = 1;
        // 对二维数组做操作
        for (int r = 0; r < row; r++) {
            for (int c = 0; c < column; c++) {
                if (obstacleGrid[r][c] == 1) {
                    nums[r][c] = 0;
                } else if (r > 0 && c > 0) {
                    nums[r][c] = nums[r - 1][c] + nums[r][c - 1];
                } else if (r > 0) {
                    nums[r][c] = nums[r - 1][c];
                } else if (c > 0) {
                    nums[r][c] = nums[r][c - 1];
                }
            }
        }
        return nums[row - 1][column - 1];
    }
}

64. 最小パス合計

ここに画像の説明を挿入
トピック分析: 動的計画法の手順に従って問題を解きます。状態遷移方程式は次のとおりです: f(x,y) = min(f(x-1,y) + a[i], f(x,y-1) ) + [i])。
コードは以下のように表示されます。

/**
 * 动态规划 
 */
class Solution {
    public int minPathSum(int[][] grid) {
        int row = grid.length;
        int column = grid[0].length;
        // 申请一个结果缓存空间,缓存结果,避免重复计算
        int[][] map = new int[row][column];
        int res = helper(grid, map, row - 1, column - 1);
        return res;
    }

    public int helper(int[][] grid, int[][] map, int x, int y) {
        int sum = 0;
        if (x == 0 && y == 0) {
            return grid[0][0];
        }
        if (x > 0 && y > 0) {
            // 如果缓存里有,直接获取
            if (map[x][y] != 0) {
                sum = map[x][y];
            } else {
                // 多种情况下,选择小的路径
                sum = Math.min(helper(grid, map, x - 1, y) + grid[x][y], helper(grid, map, x, y - 1) + grid[x][y]);
                map[x][y] = sum;
            }
        }
        if (x > 0 && y == 0) {
            // 如果缓存里有,直接获取
            if (map[x][y] != 0) {
                sum = map[x][y];
            } else {
                sum = helper(grid, map, x - 1, y) + grid[x][y];
                map[x][y] = sum;
            }
        }
        if (x == 0 && y > 0) {
            // 如果缓存里有,直接获取
            if (map[x][y] != 0) {
                sum = map[x][y];
            } else {
                sum = helper(grid, map, x, y - 1) + grid[x][y];
                map[x][y] = sum;
            }
        }
        return sum;
    }
}

70. 階段を登る

ここに画像の説明を挿入
トピック分析: 動的計画法の手順に従って問題を解くことができます。状態遷移方程式は f(n) = f(n-1) + f(n-2) です。
コードは以下のように表示されます。

/**
 * 动态规划
 */
class Solution {
    public int climbStairs(int n) {
        // 申请内存,用以缓存分支结果,不用每次都计算该值,提升运行效率
        int[] nums = new int[46];
        return helper(nums, n);
    }

    public int helper(int[] nums, int n) {
        if (n == 1) {
            return 1;
        }
        if (n == 2) {
            return 2;
        }

        int n1 = 0;
        int n2 = 0;

        // 曾计算过该值,则直接使用
        if (nums[n - 1] != 0) {
            n1 = nums[n - 1];
        } else {
            n1 = helper(nums,n - 1);
            nums[n - 1] = n1;
        }

        // 曾计算过该值,则直接使用即可
        if (nums[n - 2] != 0) {
            n2 = nums[n - 2];
        } else {
            n2 = helper(nums, n - 2);
            nums[n - 2] = n2;
        }
        return n1 + n2;
    }
}

72. 距離の編集

ここに画像の説明を挿入
トピック分析: 動的プログラミングの手順に従って問題を解決します。
コードは以下のように表示されます。

/**
 * 动态规划 
 */
class Solution {
    public int minDistance(String word1, String word2) {
        int m = word1.length();
        int n = word2.length();
        int[][] dp = new int[m + 1][n + 1];
        // 初始化
        for (int i = 1; i <= m; i++) {
            dp[i][0] =  i;
        }
        for (int j = 1; j <= n; j++) {
            dp[0][j] = j;
        }
        for (int i = 1; i <= m; i++) {
            for (int j = 1; j <= n; j++) {
                // 因为dp数组有效位从1开始
                // 所以当前遍历到的字符串的位置为i-1 | j-1
                if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
                    dp[i][j] = dp[i - 1][j - 1];
                } else {
                    dp[i][j] = Math.min(Math.min(dp[i - 1][j - 1], dp[i][j - 1]), dp[i - 1][j]) + 1;
                }
            }
        }
        return dp[m][n];
    }
}

87. スクランブルストリングス

ここに画像の説明を挿入
トピック分析: 動的プログラミングの手順に従って問題を解決します。
コードは以下のように表示されます。

/**
 * 动态规划
 */
class Solution {
    // 记忆化搜索存储状态的数组
    // -1 表示 false,1 表示 true,0 表示未计算
    int[][][] memo;
    String s1, s2;

    public boolean isScramble(String s1, String s2) {
        int length = s1.length();
        this.memo = new int[length][length][length + 1];
        this.s1 = s1;
        this.s2 = s2;
        return dfs(0, 0, length);
    }

    // 第一个字符串从 i1 开始,第二个字符串从 i2 开始,子串的长度为 length,是否和谐
    public boolean dfs(int i1, int i2, int length) {
        if (memo[i1][i2][length] != 0) {
            return memo[i1][i2][length] == 1;
        }

        // 判断两个子串是否相等
        if (s1.substring(i1, i1 + length).equals(s2.substring(i2, i2 + length))) {
            memo[i1][i2][length] = 1;
            return true;
        }

        // 判断是否存在字符 c 在两个子串中出现的次数不同
        if (!checkIfSimilar(i1, i2, length)) {
            memo[i1][i2][length] = -1;
            return false;
        }
        
        // 枚举分割位置
        for (int i = 1; i < length; ++i) {
            // 不交换的情况
            if (dfs(i1, i2, i) && dfs(i1 + i, i2 + i, length - i)) {
                memo[i1][i2][length] = 1;
                return true;
            }
            // 交换的情况
            if (dfs(i1, i2 + length - i, i) && dfs(i1 + i, i2, length - i)) {
                memo[i1][i2][length] = 1;
                return true;
            }
        }

        memo[i1][i2][length] = -1;
        return false;
    }

    public boolean checkIfSimilar(int i1, int i2, int length) {
        Map<Character, Integer> freq = new HashMap<Character, Integer>();
        for (int i = i1; i < i1 + length; ++i) {
            char c = s1.charAt(i);
            freq.put(c, freq.getOrDefault(c, 0) + 1);
        }
        for (int i = i2; i < i2 + length; ++i) {
            char c = s2.charAt(i);
            freq.put(c, freq.getOrDefault(c, 0) - 1);
        }
        for (Map.Entry<Character, Integer> entry : freq.entrySet()) {
            int value = entry.getValue();
            if (value != 0) {
                return false;
            }
        }
        return true;
    }
}

91. デコード方法

ここに画像の説明を挿入
トピックの分析: 階段歩きの強化版では、動的計画法のステップに従って問題を解くことができます。現在の数値は 0: dp[i] = dp[i-2] です。現在の数値は 0 ではありません: dp[i] = dp[i-1];
コードは次のとおりです。

/**
 * 动态规划
 */
class Solution {
    public int numDecodings(String s) {
        final int length = s.length();
        if(length == 0) return 0;
        if(s.charAt(0) == '0') return 0;

        int[] dp = new int[length+1];
        dp[0] = 1;

        for(int i=0;i<length;i++){
            dp[i+1] = s.charAt(i)=='0'?0:dp[i];
            if(i > 0 && (s.charAt(i-1) == '1' || (s.charAt(i-1) == '2' && s.charAt(i) <= '6'))){
                dp[i+1] += dp[i-1];
            }
        }
        
        return dp[length];
    }
}

96. さまざまな二分探索木

ここに画像の説明を挿入
トピック分析: 動的計画法ステップに従って問題を解くことができます。状態遷移方程式は次のとおりです: G(n) = G(0) G(n-1)+G(1) (n-2)+... +G(n-1)*G(0)。
コードは以下のように表示されます。

/**
 * 动态规划
 */
class Solution {
    public int numTrees(int n) {
        if(n <= 2) return n;
        int[] dp = new int[n + 1];
        dp[0] = 1;
        dp[1] = 1;
        dp[2] = 2;

        // 外层的循环为了填充这个dp数组
        for(int i = 3; i <=n ; i++ ){
            // 内层循环用来遍历各个元素用作根的情况
            for(int j = 1; j <= i; j++){
                dp[i] += dp[j - 1] * dp[i - j];
            }
        }
        return dp[n];
    }
}

97. インターリーブされた文字列

ここに画像の説明を挿入
トピック分析: 動的プログラミングの手順に従って問題を解決します。
コードは以下のように表示されます。

/**
 * 动态规划
 */
class Solution {
    public boolean isInterleave(String s1, String s2, String s3) {
        int s1len = s1.length();
        int s2len = s2.length();
        int s3len = s3.length();

        if (s1len + s2len != s3len) return false;

        boolean[][] dp = new boolean[s1len + 1][s2len + 1];

        dp[0][0] = true;
        for (int i = 1; i <= s1len && (dp[i-1][0] && s1.charAt(i-1) == s3.charAt(i-1) ); i++) dp[i][0] = true;
        for (int i = 1; i <= s2len && (dp[0][i-1] && s2.charAt(i-1) == s3.charAt(i-1)); i++) dp[0][i] = true;

        for (int i = 1; i <= s1.length(); i++) { //s1
            for (int j = 1; j <= s2.length(); j++) { //s2

                dp[i][j] = (dp[i-1][j] && s1.charAt(i-1) == s3.charAt(i + j - 1))
                        || (dp[i][j-1] && s2.charAt(j-1) == s3.charAt(i + j -1));
            }

        }

        return dp[s1len][s2len];
    }
}

115. 異なるサブシーケンス

ここに画像の説明を挿入
トピック分析: 動的プログラミングの手順に従って問題を解決します。
コードは以下のように表示されます。

/**
 * 动态规划 
 */
class Solution {
    public int numDistinct(String s, String t) {
        // 以i-1为结尾的s子序列中出现以j-1为结尾的t的个数为dp[i][j]
        int[][] dp = new int[s.length() + 1][t.length() + 1];
        // 初始化
        for (int i = 0; i < s.length() + 1; i++) {
            dp[i][0] = 1;
        }

        for (int i = 1; i < s.length() + 1; i++) {
            for (int j = 1; j < t.length() + 1; j++) {
                if (s.charAt(i - 1) == t.charAt(j - 1)) {
                    dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];
                }else{
                    dp[i][j] = dp[i - 1][j];
                }
            }
        }
        
        return dp[s.length()][t.length()];
    }
}

118. ヤン・フイ・トライアングル

ここに画像の説明を挿入
トピック分析: 動的プログラミングの手順に従って問題を解決します。
コードは以下のように表示されます。

/**
 * 动态规划
 */
class Solution {
    public List<List<Integer>> generate(int numRows) {
        List<List<Integer>> res = new ArrayList<>();
        for (int i = 1; i <= numRows; i++) {
            // 每一行的结果
            List<Integer> list = new ArrayList<>();
            for (int j = 1; j <= i; j++) {
                // 第一列和最后一列为1
                if (j == 1 || j == i) {
                    list.add(1);
                } else {
                    list.add(res.get(i - 2).get(j - 2) + res.get(i - 2).get(j - 1));
                }
            }
            res.add(list);
        }
        return res;
    }
}

119. 楊輝トライアングルⅡ

ここに画像の説明を挿入
トピック分析: 動的プログラミングの手順に従って問題を解決します。
コードは以下のように表示されます。

/**
 * 动态规划
 */
class Solution {
    public List<Integer> getRow(int rowIndex) {
        List<Integer> res = new ArrayList<>(rowIndex + 1);
        long cur = 1;
        for (int i = 0; i <= rowIndex; i++) {
            res.add((int) cur);
            cur = cur * (rowIndex - i) / (i + 1);
        }
        return res;
    }
}

120. 三角形の最小パス和

ここに画像の説明を挿入
トピック分析: 動的計画法手順に従って問題、状態遷移方程式を解決します: dp[j] = Math.min(dp[j],dp[j+1]) + curTr.get(j);。
コードは以下のように表示されます。

/**
 * 动态规划
 */
class Solution {
    public int minimumTotal(List<List<Integer>> triangle) {
        if (triangle == null || triangle.size() == 0){
            return 0;
        }
        // 滚动记录每一层的最小值
        int[] dp = new int[triangle.size()+1];

        for (int i = triangle.size() - 1; i >= 0; i--) {
            List<Integer> curTr = triangle.get(i);
            for (int j = 0; j < curTr.size(); j++) {
                // 这里的dp[j] 使用的时候默认是上一层的,赋值之后变成当前层
                dp[j] = Math.min(dp[j],dp[j+1]) + curTr.get(j);
            }
        }
        return dp[0];
    }
}

121. 株を売買するのに最適な時期

ここに画像の説明を挿入
トピック分析: 動的計画法に従って問題を解くことができます状態遷移方程式は次のとおりです: 過去 i 日間の最大収入 = max{過去 i-1 日間の最大収入, i 番目の価格day-前の i-1 日間の最低価格 }。
コードは以下のように表示されます。

/**
 * 动态规划
 */
class Solution {
    public int maxProfit(int[] prices) {
        if (prices.length <= 1) {
            return 0;
        }
        int min = prices[0], max = 0;
        for (int i = 1; i < prices.length; i++) {
            // 状态转移方程式
            max = Math.max(max, prices[i] - min);
            // 更新最小值
            min = Math.min(min, prices[i]);
        }
        return max;
    }
}

122. 株を売買するのに最適な時期 II

ここに画像の説明を挿入
トピック分析: 動的プログラミングの手順に従って問題を解決します。
コードは以下のように表示されます。

/**
 * 动态规划
 */
class Solution {
    public int maxProfit(int[] prices) {
        int n = prices.length;
        int[][] dp = new int[n][2];
        dp[0][0] = 0;
        dp[0][1] = -prices[0];
        for (int i = 1; i < n; ++i) {
            dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i]);
            dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i]);
        }
        return dp[n - 1][0];
    }
}

123. 株を売買するのに最適な時期 III

ここに画像の説明を挿入
トピック分析: 動的プログラミングの手順に従って問題を解決します。
コードは以下のように表示されます。

/**
 * 动态规划
 */
class Solution {

    public int maxProfit(int[] prices) {
        int len = prices.length;
        // 边界判断, 题目中 length >= 1, 所以可省去
        if (prices.length == 0) return 0;
        // dp[i][j] 中i表示第i天,j为[0 - 4] 五个状态,dp[i][j]表示第i天状态j所剩最大现金
        int[][] dp = new int[len][5];
        dp[0][1] = -prices[0];
        // 初始化第二次买入的状态,是为了确保最后结果是最多两次买卖的最大利润
        dp[0][3] = -prices[0];

        for (int i = 1; i < len; i++) {
            dp[i][1] = Math.max(dp[i - 1][1], -prices[i]);
            dp[i][2] = Math.max(dp[i - 1][2], dp[i][1] + prices[i]);
            dp[i][3] = Math.max(dp[i - 1][3], dp[i][2] - prices[i]);
            dp[i][4] = Math.max(dp[i - 1][4], dp[i][3] + prices[i]);
        }

        return dp[len - 1][4];
    }
}

124. 二分木における最大パス和

ここに画像の説明を挿入
トピック分析: 動的プログラミングの手順に従って問題を解決します。
コードは以下のように表示されます。

/**
 * 动态规划
 */
class Solution {
    int maxSum = Integer.MIN_VALUE;

    public int maxPathSum(TreeNode root) {
        maxGain(root);
        return maxSum;
    }

    public int maxGain(TreeNode node) {
        if (node == null) {
            return 0;
        }
        
        // 递归计算左右子节点的最大贡献值
        // 只有在最大贡献值大于 0 时,才会选取对应子节点
        int leftGain = Math.max(maxGain(node.left), 0);
        int rightGain = Math.max(maxGain(node.right), 0);

        // 节点的最大路径和取决于该节点的值与该节点的左右子节点的最大贡献值
        int priceNewpath = node.val + leftGain + rightGain;

        // 更新答案
        maxSum = Math.max(maxSum, priceNewpath);

        // 返回节点的最大贡献值
        return node.val + Math.max(leftGain, rightGain);
    }
}

132. 分割回文Ⅱ

ここに画像の説明を挿入
トピック分析: 動的プログラミングの手順に従って問題を解決します。
コードは以下のように表示されます。

/**
 * 动态规划
 */
class Solution {
    public int minCut(String s) {
        int n = s.length();
        boolean[][] g = new boolean[n][n];
        for (int i = 0; i < n; ++i) {
            Arrays.fill(g[i], true);
        }

        for (int i = n - 1; i >= 0; --i) {
            for (int j = i + 1; j < n; ++j) {
                g[i][j] = s.charAt(i) == s.charAt(j) && g[i + 1][j - 1];
            }
        }

        int[] f = new int[n];
        Arrays.fill(f, Integer.MAX_VALUE);
        for (int i = 0; i < n; ++i) {
            if (g[0][i]) {
                f[i] = 0;
            } else {
                for (int j = 0; j < i; ++j) {
                    if (g[j + 1][i]) {
                        f[i] = Math.min(f[i], f[j] + 1);
                    }
                }
            }
        }

        return f[n - 1];
    }
}

139. 単語の分割

ここに画像の説明を挿入
トピック分析: 動的プログラミングのステップに従って問題を解決します。状態遷移方程式は次のとおりです: if (wordDict.contains(s.substring(j,i)) && valid[j]) valid[i] = true;。
コードは以下のように表示されます。

/**
 * 动态规划
 */
class Solution {
    public boolean wordBreak(String s, List<String> wordDict) {
        // 表示能否拆分为一个或多个在字典中出现的单词
        boolean[] valid = new boolean[s.length() + 1];
        valid[0] = true;
        for (int i = 1; i <= s.length(); i++) {
            for (int j = 0; j < i; j++) {
                if (wordDict.contains(s.substring(j,i)) && valid[j]) {
                    valid[i] = true;
                }
            }
        }
        return valid[s.length()];
    }
}

174. ダンジョンゲーム

ここに画像の説明を挿入
トピック分析: 動的プログラミングの手順に従って問題を解決します。
コードは以下のように表示されます。

/**
 * 动态规划
 */
class Solution {
    public int calculateMinimumHP(int[][] dungeon) {
        int n = dungeon.length, m = dungeon[0].length;
        int[][] dp = new int[n + 1][m + 1];
        for (int i = 0; i <= n; ++i) {
            Arrays.fill(dp[i], Integer.MAX_VALUE);
        }
        dp[n][m - 1] = dp[n - 1][m] = 1;
        for (int i = n - 1; i >= 0; --i) {
            for (int j = m - 1; j >= 0; --j) {
                int minn = Math.min(dp[i + 1][j], dp[i][j + 1]);
                dp[i][j] = Math.max(minn - dungeon[i][j], 1);
            }
        }
        return dp[0][0];
    }
}

188. 株を売買するのに最適な時期 IV

ここに画像の説明を挿入
トピック分析: 動的プログラミングの手順に従って問題を解決します。
コードは以下のように表示されます。

/**
 * 动态规划
 */
class Solution {
    public int maxProfit(int k, int[] prices) {
        if (prices.length == 0) {
            return 0;
        }

        int n = prices.length;
        k = Math.min(k, n / 2);
        int[][] buy = new int[n][k + 1];
        int[][] sell = new int[n][k + 1];

        buy[0][0] = -prices[0];
        sell[0][0] = 0;
        for (int i = 1; i <= k; ++i) {
            buy[0][i] = sell[0][i] = Integer.MIN_VALUE / 2;
        }

        for (int i = 1; i < n; ++i) {
            buy[i][0] = Math.max(buy[i - 1][0], sell[i - 1][0] - prices[i]);
            for (int j = 1; j <= k; ++j) {
                buy[i][j] = Math.max(buy[i - 1][j], sell[i - 1][j] - prices[i]);
                sell[i][j] = Math.max(sell[i - 1][j], buy[i - 1][j - 1] + prices[i]);   
            }
        }

        return Arrays.stream(sell[n - 1]).max().getAsInt();
    }
}

198.強盗

ここに画像の説明を挿入
トピック分析: 動的計画法ステップ、状態遷移方程式: dp[i]=max(dp[i−2]+nums[i],dp[i−1]) に従って問題を解きます。
コードは以下のように表示されます。

/**
 * 动态规划
 */
class Solution {
    public int rob(int[] nums) {
        int length = nums.length;
        if (nums == null || length == 0) {
            return 0;
        }
        if (length == 1) {
            return nums[0];
        }
        int[] dp = new int[length];
        dp[0] = nums[0];
        dp[1] = Math.max(nums[0], nums[1]);
        for (int i = 2; i < length; i++) {
            dp[i] = Math.max(dp[i - 2] + nums[i], dp[i - 1]);
        }
        return dp[length - 1];
    }
}

213. 打家劫舍 II

ここに画像の説明を挿入
トピック分析: 動的計画法の手順に従って問題を解決できますが、元のベースでは、最初または最後の配列を削除して、再度入力することを検討してください。
コードは以下のように表示されます。

/**
 * 动态规划
 */
class Solution {
    public int rob(int[] nums) {
        if (nums == null || nums.length == 0)
            return 0;
        int len = nums.length;
        if (len == 1)
            return nums[0];
        return Math.max(robAction(nums, 0, len - 1), robAction(nums, 1, len));
    }

    int robAction(int[] nums, int start, int end) {
        int x = 0, y = 0, z = 0;
        for (int i = start; i < end; i++) {
            y = z;
            z = Math.max(y, x + nums[i]);
            x = y;
        }
        return z;
    }
}

221. 最大の広場

ここに画像の説明を挿入
トピック分析: 動的プログラミングの手順に従って問題を解決します。
コードは以下のように表示されます。

/**
 * 动态规划 
 */
class Solution {
    public int maximalSquare(char[][] matrix) {
        int m = matrix.length;
        if(m < 1) return 0;
        int n = matrix[0].length;
        int max = 0;
        // 表示以第i行第j列为右下角所能构成的最大正方形边长
        int[][] dp = new int[m+1][n+1];
        
        for(int i = 1; i <= m; ++i) {
            for(int j = 1; j <= n; ++j) {
                if(matrix[i-1][j-1] == '1') {
                    dp[i][j] = 1 + Math.min(dp[i-1][j-1], Math.min(dp[i-1][j], dp[i][j-1]));
                    max = Math.max(max, dp[i][j]); 
                }
            }
        }
        
        return max * max;
    }
}

264. 丑数 II

ここに画像の説明を挿入
トピック分析: 動的プログラミングの手順に従って問題を解決します。醜い数値は、前の醜い数値に 2、または 3、または 5 を乗算して取得する必要があります。3 つのポインターを左から右に使用し、毎回かかります最小の醜い番号が次の番号です。
コードは以下のように表示されます。

/**
 * 动态规划
 */
class Solution {
    public int nthUglyNumber(int n) {
        int[] dp = new int[n + 1];
        dp[1] = 1;
        int p2 = 1, p3 = 1, p5 = 1;
        for (int i = 2; i <= n; i++) {
            int num2 = dp[p2] * 2, num3 = dp[p3] * 3, num5 = dp[p5] * 5;
            dp[i] = Math.min(Math.min(num2, num3), num5);
            if (dp[i] == num2) {
                p2++;
            }
            if (dp[i] == num3) {
                p3++;
            }
            if (dp[i] == num5) {
                p5++;
            }
        }
        return dp[n];
    }
}

279. 完全正方形

ここに画像の説明を挿入
トピック分析: 動的プログラミングの手順に従って問題を解決します。
コードは以下のように表示されます。

/**
 * 动态规划
 */
class Solution {
    public int numSquares(int n) {
        int max = Integer.MAX_VALUE;
        // 和为i的完全平方数的最少数量为dp[i]
        int[] dp = new int[n + 1];
        // 初始化
        for (int j = 0; j <= n; j++) {
            dp[j] = max;
        }
        // 当和为0时,组合的个数为0
        dp[0] = 0;
        // 遍历物品
        for (int i = 1; i * i <= n; i++) {
            // 遍历背包
            for (int j = i * i; j <= n; j++) {
                if (dp[j - i * i] != max) {
                    dp[j] = Math.min(dp[j], dp[j - i * i] + 1);
                }
            }
        }
        return dp[n];
    }
}

300. 最長の増加サブシーケンス

ここに画像の説明を挿入
トピック分析: 動的計画法ステップに従って問題を解決します。状態遷移方程式は次のとおりです: dp[i] = max(dp[i], dp[j] + 1)。
コードは以下のように表示されます。

/**
 * 动态规划
 */
class Solution {
    public int lengthOfLIS(int[] nums) {
        int[] dp = new int[nums.length];
        Arrays.fill(dp, 1);
        int res = 0;
        for (int i = 0; i < dp.length; i++) {
            for (int j = 0; j < i; j++) {
                if (nums[i] > nums[j]) {
                    dp[i] = Math.max(dp[i], dp[j] + 1);
                }
            }
            // 取最长的子序列
            if(dp[i] > res) res = dp[i];
        }
        return res;
    }
}

ホームページに戻る

Leetcode 500 以上の質問を磨くことについての感想

「アルゴリズムシリーズ」のデザイン

おすすめ

転載: blog.csdn.net/qq_22136439/article/details/126798330