第二章 动态规划

154 阅读9分钟

最长递增子序列

题目

image.png

版本1 错误

    // 输入一个无序数组, 找到一个最长递增子序列的长度
    public int lengthOfLIS(int [] nums) {
        if (nums.length == 0) {
            return 0;
        }

        // 明确状态
        // 状态就是长度为i的子数组, i为0,1,2....n

        // 明确dp[i]含义, 表示0到i, 长度为i + 1的子数组中
        // 最长递增子序列是以i结尾的
        // 此时的最长递增子序列的长度为x, 即dp[i] = x;
        int [] dp = new int[nums.length];

        // base case
        // 长度为1的子数组, 最长递归子序列的长度只能为1
        dp[0] = 1;

        // 枚举所有状态
        int max = Integer.MIN_VALUE;
        for (int i = 1; i < nums.length; i ++) {
            // 在当前状态下, 枚举所有可能到达此状态的情况
            // 以i结尾, 那么在i之前所有的j, j < i, 如果nums[j] < nums[i], 那么都有可能参与构成递增序列
            // 寻找最大的dp[j]
            dp[i] = 1;
            for (int j = i - 1; j >= 0; j --) {
                if (nums[j] < nums[i]) {
                    dp[i] = Math.max(dp[i], dp[j] + 1);
                }
            }

            // 每次保留最大值
            max = Math.max(dp[i], max);

        }
        return max;
    }

错误示例

image.png

错误原因

(1) 枚举所有状态时, 是从1开始的, 因此数组长度为1时, 是直接返回的max的默认值, 导致出错.

版本2 正确

    // 输入一个无序数组, 找到一个最长递增子序列的长度
    public int lengthOfLIS(int [] nums) {
        if (nums.length == 0) {
            return 0;
        }
        if (nums.length == 1) {
            return 1;
        }

        // 明确状态
        // 状态就是长度为i的子数组, i为0,1,2....n

        // 明确dp[i]含义, 表示0到i, 长度为i + 1的子数组中
        // 最长递增子序列是以i结尾的
        // 此时的最长递增子序列的长度为x, 即dp[i] = x;
        int [] dp = new int[nums.length];

        // base case
        // 长度为1的子数组, 最长递归子序列的长度只能为1
        dp[0] = 1;

        // 枚举所有状态
        int max = Integer.MIN_VALUE;
        for (int i = 1; i < nums.length; i ++) {
            // 在当前状态下, 枚举所有可能到达此状态的情况
            // 以i结尾, 那么在i之前所有的j, j < i, 如果nums[j] < nums[i], 那么都有可能参与构成递增序列
            // 寻找最大的dp[j]
            dp[i] = 1;
            for (int j = i - 1; j >= 0; j --) {
                if (nums[j] < nums[i]) {
                    dp[i] = Math.max(dp[i], dp[j] + 1);
                }
            }

            // 每次保留最大值
            max = Math.max(dp[i], max);

        }
        return max;
    }

俄罗斯套娃信封问题

题目

image.png

版本1 正确

    // 信封嵌套问题, 求信封最多能嵌套的次数
    int maxEnvelopes(int [][] envelopes) {

        // 明确状态
        // 信封的宽度w 和 高度h
        // 明面上的状态是2个, 但是如果我们将信封按照宽度升序排列
        // 然后对应的高度会组成一个新的排列, 此时高度大的一定能够套住高度小的.
        // 就变成了求高度排列的最长递增子序列的长度.

        // 如果出现宽度相同的情况, 为了避免相同宽度, 但是高度不同的信封, 大的套小的
        // 将宽度相同, 高度不同的信封, 在排列高度时, 按照降序排列。
        int n = envelopes.length;

        // 二维数组排序, 利用api方法。
        // Arrays.sort()方法默认是升序排列
        Arrays.sort(envelopes, new Comparator<int[]>() {
            @Override
            public int compare(int[] o1, int[] o2) {
                if (o1[0] == o2[0]) {
                    // 宽度相等, 高度按照降序排序
                    return o2[1] - o1[1];
                }
                // 宽度大的, 排在后面, 即升序排列
                return o1[0] - o2[0];
            }
        });

        // 提取高度数组
        int [] height = new int[n];
        for (int i = 0; i < n; i ++) {
            height[i] = envelopes[i][1];
        }


        // 明确dp数组的含义
        // 此时的dp数组, 就变成求最长递增子序列的长度问题了, 不再赘述。
        return lengthOfLIS(height);
    }

    public int lengthOfLIS(int [] nums) {
        if (nums.length == 0) {
            return 0;
        }
        if (nums.length == 1) {
            return 1;
        }
        int [] dp = new int[nums.length];
        dp[0] = 1;
        int max = Integer.MIN_VALUE;
        for (int i = 1; i < nums.length; i ++) {
            dp[i] = 1;
            for (int j = i - 1; j >= 0; j --) {
                if (nums[j] < nums[i]) {
                    dp[i] = Math.max(dp[i], dp[j] + 1);
                }
            }
            max = Math.max(dp[i], max);

        }
        return max;
    }

注意点

(1) 注意问题的转化(前提是不能旋转信封)

(2) 注意二维数组排序的写法. 注意Arrays.sort方法的默认排序为升序.

连续子数组的最大和

题目

image.png

版本1 错误

// 和最大的子数组 元素有正有负
    public int maxSubArray(int [] nums) {
        if (nums.length == 0) {
            return 0;
        }
        if (nums.length == 1) {
            return nums[0];
        }

        // 明确状态
        // 状态i就是元素的个数, i表示0...i子数组

        // 明确dp[i]的含义
        // dp[i] = x, 就是在0,1, 2....i的子数组中
        // 以i结尾的连续数组的和的最大值为x
        int [] dp = new int[nums.length];

        // base case
        dp[0] = nums[0];

        // 枚举所有状态
        int max = Integer.MIN_VALUE;
        for(int i = 1; i < nums.length; i ++) {
            // 因为是子数组, 所以必须是连续的
            // 如果dp[i - 1]为正数, 表示有贡献, 就加上, 否则就是nums[i]
            dp[i] = Math.max(0, dp[i - 1]) + nums[i];
            max = Math.max(dp[i], max);
        }

        return max;
    }

错误的原因

image.png (1) 如果数组长度为2, 同时最大值就是数组的第一个元素, 这种写法, max不会去取值dp[0], 因此出错.

原因是因为比较的时候使用的是Math.max(0, dp[i - 1])

版本2 正确

    // 和最大的子数组 元素有正有负
    public int maxSubArray(int [] nums) {
        if (nums.length == 0) {
            return 0;
        }
        if (nums.length == 1) {
            return nums[0];
        }

        // 明确状态
        // 状态i就是元素的个数, i表示0...i子数组

        // 明确dp[i]的含义
        // dp[i] = x, 就是在0,1, 2....i的子数组中
        // 以i结尾的连续数组的和的最大值为x
        int [] dp = new int[nums.length];

        // base case
        dp[0] = nums[0];

        // 枚举所有状态
        int max = dp[0];
        for(int i = 1; i < nums.length; i ++) {
            // 因为是子数组, 所以必须是连续的
            // 如果dp[i - 1]为正数, 表示有贡献, 就加上, 否则就是nums[i]
            dp[i] = Math.max(0, dp[i - 1]) + nums[i];
            max = Math.max(dp[i], max);
        }

        return max;
    }

正确的原因

(1) 将max的初始值设为nums[0], 避免出现最大值为负数的情况出错

最长公共序列

题目

image.png

版本1 错误

    // 最长公共子序列
    public int longestCommonSubsequence(String text1, String text2) {
        if (text1.length() == 0 || text2.length() == 0) {
            return text1.length() + text2.length();
        }

        // 明确状态
        // i和j 分别表示在text1中取前i个字符, 即0,1,2..i的字符串
        // j表示在text2中取前j个字符, 即0,1,2..j的字符串

        // 明确dp数组的含义
        // dp[i][j] = x表示 在text1中前i个字符, 和text2中前j个字符, 构成的最长公共子序列长度为x
        int [][] dp = new int[text1.length()][text2.length()];

        // base case
        // dp[0][j] = 0; dp[i][0] = 0; 无需额外设置

        // 枚举所有状态
        for (int i = 1; i < text1.length(); i ++) {
            for (int j = 1; j < text2.length(); j ++) {
                if (text1.charAt(i) == text2.charAt(j)) {
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                } else {
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
                }
            }
        }

        return dp[text1.length() - 1][text2.length() - 1];

    }

错误的原因

(1) 如果dp数组的长度和字符串长度一致, 那么这里的base case写的就不对. 因为dp[0][j]表示的其实是text的第一个字符和text所有字符的关系, 并不是想象的那样.

(2) 因此dp数组和原始条件长度一致时, 一定要注意Base case到底是什么, 这里的base case 应该是dp[0][0], dp[0][1], dp[1][0], 这三个初识值作为条件, 必须手动算出来

版本2 正确

    // 最长公共子序列
    public int longestCommonSubsequence(String text1, String text2) {
        if (text1.length() == 0 || text2.length() == 0) {
            return text1.length() + text2.length();
        }

        // 明确状态
        // i和j 分别表示在text1中取前i个字符, 即0,1,2..i的字符串
        // j表示在text2中取前j个字符, 即0,1,2..j的字符串

        // 明确dp数组的含义
        // dp[i][j] = x表示 在text1中前i个字符, 和text2中前j个字符, 构成的最长公共子序列长度为x
        // dp数组在构造时多增加一位长度, base case就会变的很简单
        int [][] dp = new int[text1.length() + 1][text2.length() + 1];

        // base case
        // dp[0][j] = 0; dp[i][0] = 0; 无需额外设置

        // 枚举所有状态
        for (int i = 1; i <= text1.length(); i ++) {
            for (int j = 1; j <= text2.length(); j ++) {
                if (text1.charAt(i - 1) == text2.charAt(j - 1)) {
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                } else {
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
                }
            }
        }

        return dp[text1.length()][text2.length()];

    }

编辑距离

题目

image.png

版本1 错误

    public int minDistance(String word1, String word2) {
        if (word1.length() == 0 || word2.length() == 0) {
            return word1.length() + word2.length();
        }

        // 明确状态
        // 两个字符串, 就有两个状态i和j
        // 分为表示在word1, 和 word2中指的位置

        // 明确dp数组含义
        // dp[i][j] = x表示word1前i个字符, 变成word2前j个字符所需要的最少操作步骤.
        int [][] dp = new int[word1.length() + 1][word2.length() + 1];

        // base case
        // dp[0][j] 如果word1中没有字符, 变成word2的任意j, 就需要插入j次
        for(int j = 0; j < word2.length(); j ++) {
            dp[0][j] = j;
        }

        // 同理dp[i][0] 需要删除i次
        for(int i = 0; i < word1.length(); i ++) {
            dp[i][0] = i;
        }

        // 枚举所有状态
        // 将word1变成word2
        for(int i = 1; i <= word1.length(); i ++) {
            for (int j = 1; j <= word2.length(); j ++) {
                if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
                    // 啥也不用做
                    dp[i][j] = dp[i - 1][j - 1];
                } else {
                    // 因为已经计算到i和j了, 所以此时word1和word2前i - 1个和前j - 1个一定是相同的了。
                    // 假设现在word1是abce, i指向e
                    // word2是abcd, j指向d
                    // 现在必须执行插入操作
                    // 那么word1变成abcde, 发现word1的长度不再是i了, 不符合dp的定义
                    // 因此word1要想执行插入操作, 必须先删除了i元素, 才可以。
                    // 那么wod1删除i元素就是子问题当word1 = abce和word2 = abc的时候, dp的结果
                    // 就是dp[i][j - 1];
                    // 因此如果必须执行插入操作的话, 就必须由子问题dp[i][j - 1]的答案得到
                    int insert = dp[i][j - 1] + 1;
                    // 选择替换
                    int replace = dp[i - 1][j - 1] + 1;
                    // 选择删除 同选择查找的逻辑
                    int del = dp[i - 1][j] + 1;

                    dp[i][j] = Math.min(insert, Math.min(replace, del));
                }
            }
        }

        return dp[word1.length()][word2.length()];


    }

错误的原因

(1) 注意如果要执行插入操作, 必须要先执行一次删除, 才能执行插入. 同理, 如果要删除, 必须先执行一次插入. 因此会转化成相应的子问题.

(2) base case那里写的有问题, 当i == word1.length()和 j = word2.length()的时候都没包含上, 到底有些结果是错误的. 淦

版本2 正确

    public int minDistance(String word1, String word2) {
        if (word1.length() == 0 || word2.length() == 0) {
            return word1.length() + word2.length();
        }

        // 明确状态
        // 两个字符串, 就有两个状态i和j
        // 分为表示在word1, 和 word2中指的位置

        // 明确dp数组含义
        // dp[i][j] = x表示word1前i个字符, 变成word2前j个字符所需要的最少操作步骤.
        int [][] dp = new int[word1.length() + 1][word2.length() + 1];

        // base case
        // dp[0][j] 如果word1中没有字符, 变成word2的任意j, 就需要插入j次
        // 注意j可以取word2.length()
        for(int j = 0; j <= word2.length(); j ++) {
            dp[0][j] = j;
        }

        // 同理dp[i][0] 需要删除i次
        // 注意i可以取word1.length()
        for(int i = 0; i <= word1.length(); i ++) {
            dp[i][0] = i;
        }

        // 枚举所有状态
        // 将word1变成word2
        for(int i = 1; i <= word1.length(); i ++) {
            for (int j = 1; j <= word2.length(); j ++) {
                if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
                    // 啥也不用做
                    dp[i][j] = dp[i - 1][j - 1];
                } else {
                    // 因为已经计算到i和j了, 所以此时word1和word2前i - 1个和前j - 1个一定是相同的了。
                    // 假设现在word1是abce, i指向e
                    // word2是abcd, j指向d
                    // 现在必须执行插入操作
                    // 那么word1变成abcde, 发现word1的长度不再是i了, 不符合dp的定义
                    // 因此word1要想执行插入操作, 必须先删除了i元素, 才可以。
                    // 那么wod1删除i元素就是子问题当word1 = abce和word2 = abc的时候, dp的结果
                    // 就是dp[i][j - 1];
                    // 因此如果必须执行插入操作的话, 就必须由子问题dp[i][j - 1]的答案得到
                    int insert = dp[i][j - 1] + 1;
                    // 选择替换
                    int replace = dp[i - 1][j - 1] + 1;
                    // 选择删除 同选择查找的逻辑
                    int del = dp[i - 1][j] + 1;

                    dp[i][j] = Math.min(insert, Math.min(replace, del));
                }
            }
        }

        return dp[word1.length()][word2.length()];


    }

最长回文子序列

题目

image.png

版本1 错误

    // 最长回文子序列
    public int longestPalindromeSubseq(String s) {

        // 明确状态
        // i和j表示字符串s, i到j这一段子串

        // 明确dp数组含义
        // dp[i][j] = x 表示字符串s中, 索引下标在i....j 这段子串的最长回文序列的长度x
        int [][] dp = new int[s.length()][s.length()];

        // base case
        for (int i = 0; i < s.length(); i ++) {
            for (int j = 0; j < s.length(); j ++) {
                dp[i][j] = 1;
            }
        }

        // 枚举所有状态.
        // 因为base是对角线上, 因此枚举的时候, 也需要斜着枚举
        // 这样子问题的答案才有值

        for(int l = 1; l < s.length(); l ++) {
            // l表示两个索引之间的距离
            // base case设置了l = 0的情况
            for (int i = 0; i < s.length() - 1; i ++) {
                int j = i + l;
                if (j > s.length() - 1) {
                    break;
                }
                if (s.charAt(i) == s.charAt(j)) {
                    // 当l = 1的时候, 虽然此时的j > i了, 但是因为此时的dp[i][j]=0, 所以没关系
                    dp[i][j] = dp[i + 1][j - 1] + 2;
                } else {
                    // 如果元素不想等, 因为求的是回文子序列
                    // 因此不像子数组那样会直接失败, 而是有值的
                    dp[i][j] = dp[i + 1][j - 1];
                }
            }
        }

        return dp[0][s.length() - 1];
    }

错误的原因

(1) 当元素不相同的时候, 不应该直接舍弃两步的元素dp[i][j] = dp[i + 1][j - 1];, 而是应该在两边分别舍弃元素, 取最大值

版本2 错误

    // 最长回文子序列
    public int longestPalindromeSubseq(String s) {

        // 明确状态
        // i和j表示字符串s, i到j这一段子串

        // 明确dp数组含义
        // dp[i][j] = x 表示字符串s中, 索引下标在i....j 这段子串的最长回文序列的长度x
        int [][] dp = new int[s.length()][s.length()];

        // base case
        for (int i = 0; i < s.length(); i ++) {
            for (int j = 0; j < s.length(); j ++) {
                dp[i][j] = 1;
            }
        }

        // 枚举所有状态.
        // 因为base是对角线上, 因此枚举的时候, 也需要斜着枚举
        // 这样子问题的答案才有值

        for(int l = 1; l < s.length(); l ++) {
            // l表示两个索引之间的距离
            // base case设置了l = 0的情况
            for (int i = 0; i < s.length() - 1; i ++) {
                int j = i + l;
                if (j > s.length() - 1) {
                    break;
                }
                if (s.charAt(i) == s.charAt(j)) {
                    // 当l = 1的时候, 虽然此时的j > i了, 但是因为此时的dp[i][j]=0, 所以没关系
                    dp[i][j] = dp[i + 1][j - 1] + 2;
                } else {
                    // 如果元素不想等, 因为求的是回文子序列
                    // 因此不像子数组那样会直接失败, 而是有值的
                    // 需要在两边舍弃元素
                    dp[i][j] = Math.max(dp[i + 1][j], dp[i][j - 1]);
                }
            }
        }

        return dp[0][s.length() - 1];
    }
    

错误的原因

(1) base case那里写错了, 这种写法是将整个数组全都赋值成1了, 而我们期望的是i==j的时候才是1

版本3

    // 最长回文子序列
    public static int longestPalindromeSubseq(String s) {

        // 明确状态
        // i和j表示字符串s, i到j这一段子串

        // 明确dp数组含义
        // dp[i][j] = x 表示字符串s中, 索引下标在i....j 这段子串的最长回文序列的长度x
        int [][] dp = new int[s.length()][s.length()];

        // base case
        for (int i = 0; i < s.length(); i ++) {
            for (int j = 0; j < s.length(); j ++) {
                if (i == j) {
                    dp[i][j] = 1;
                }

            }
        }

        // 枚举所有状态.
        // 因为base是对角线上, 因此枚举的时候, 也需要斜着枚举
        // 这样子问题的答案才有值

        for(int l = 1; l < s.length(); l ++) {
            // l表示两个索引之间的距离
            // base case设置了l = 0的情况
            for (int i = 0; i < s.length() - 1; i ++) {
                int j = i + l;
                if (j > s.length() - 1) {
                    break;
                }
                if (s.charAt(i) == s.charAt(j)) {
                    // 当l = 1的时候, 虽然此时的j > i了, 但是因为此时的dp[i][j]=0, 所以没关系
                    dp[i][j] = dp[i + 1][j - 1] + 2;
                } else {
                    // 如果元素不想等, 因为求的是回文子序列
                    // 因此不像子数组那样会直接失败, 而是有值的
                    // 需要在两边舍弃元素
                    dp[i][j] = Math.max(dp[i + 1][j], dp[i][j - 1]);
                }
            }
        }

        return dp[0][s.length() - 1];
    }

让字符串成为回文串的最少插入次数

题目

image.png

版本1 正确

// 让字符串成为回文串的最小插入次数
    public int minInsertions(String s) {
        if (s.length() == 0 || s.length() == 1) {
            return 0;
        }

        // 明确状态 i和j表示字符串s中的索引
        // i, i + 1, ...., j -1, j就表示s的一个子串

        // 明确dp数组的含义
        // dp[i][j] = x, 就表示使得i到j的子串构成回文串需要的最少插入次数是x
        int [][] dp = new int[s.length()][s.length()];

        // base case
        // 当i == j 的时候, dp[i][j] = 0; 已经满足


        // 枚举状态
        // 需要歇着遍历
        for (int l = 1; l < s.length(); l ++) {
            for (int i = 0; i < s.length(); i ++) {
                int j = i + l;
                if (j >= s.length()) {
                    break;
                }
                if (s.charAt(i) == s.charAt(j)) {
                    // 无需插入操作
                    dp[i][j] = dp[i + 1][j - 1];
                } else {
                    // 如果想让i....j是回文串, 并且i + 1, ...., j - 1已经是回文串了
                    // 那么只需要在i + 1,...j的基础上插入一个字符即可
                    // 或者在i,...j - 1的基础上插入一个字符
                    // 最差情况是在i + 1,...,j - 1的基础上插入两个字符, 但是已经包含在上述情况中了
                    dp[i][j] = Math.min(dp[i + 1][j], dp[i][j - 1]) + 1;
                }

            }
        }
        
        return dp[0][s.length() - 1];
    }

正则表达式

题目

image.png

版本1 错误

    public boolean isMatch(String s, String p) {

        // 明确状态
        // 两个指针, 一个i指向s中的字符, 一个j指向p中的字符

        // 明确dp数组的含义
        // dp[i][j] = x, 表示s中的前i个字符, 能否被p中的前j个字符匹配, 匹配x为true
        boolean [][] dp = new boolean[s.length()][p.length()];

        // base case


        // 枚举状态
        for(int i = 0; i < s.length(); i ++) {
            for (int j = 0; j < p.length(); j ++) {
                if (p.charAt(j) == '.' || p.charAt(j) == s.charAt(i)) {
                    dp[i][j] = Boolean.TRUE;
                    
                } else {

                }
            }
        }

    }

错误的原因

(1) 这里, 当前的问题, 无法由已经解决了的子问题得到, 因此自底向上的方式是不行的, 只能通过递归 + 备忘录的方式解决.

版本2 错误

 Map<String, Boolean> visited = new HashMap<>();
    public boolean isMatch(String s, String p) {

        // 明确状态
        // 两个指针, 一个i指向s中的字符, 一个j指向p中的字符

        // 明确递归函数的含义
        // digui(s, i, p, j) 表示s中的从i开始的所有字符, 能否被p中从j开始的所有字符匹配, 匹配x为true
        return digui(s, 0, p, 0);
    }

    public boolean digui(String s, int i, String p, int j) {
        
        if (visited.containsKey(i + ":" + j)) {
            return visited.get(i + ":" + j);
        }
        
        // base case
        if (j == p.length()) {
            // 匹配字符串走到末尾的时候, 待匹配字符串是否匹配完毕了
            return i == s.length();
        }

        boolean ans;
        if (s.charAt(i) == p.charAt(j) || p.charAt(j) == '.') {
            // p的下一个字符是不是*
            if (j + 1 < p.length() && p.charAt(j + 1) == '*') {
                // 如果是* 就可以匹配0次或者多次
                ans = digui(s, i + 1, p, j) || digui(s, i, p, j + 2);
            } else {
                // 当前匹配成功, 继续匹配下一个
                ans = digui(s, i + 1, p, j + 1);
            }
        } else {
            // p的下一个字符是不是*
            if (j + 1 < p.length() && p.charAt(j + 1) == '*') {
                // 如果是* 就可以是0次
                ans = digui(s, i , p, j + 2);
            } else {
                // 匹配失败
                ans = Boolean.FALSE;
            }
        }
        visited.put(i + ":" + j, ans);
        return ans;

    }

错误的原因

(1) i一定是最多加1, 但是关于i没有定义base case, 就会导致i数组越界

(2) j有+2的情况, 但是都是j的下一个是*的情况, 也就是说j + 2最多也就是等于p.length(), 不会超出p.length()的数值

版本3 正确

class Solution {
    Map<String, Boolean> visited = new HashMap<>();
    public boolean isMatch(String s, String p) {

        // 明确状态
        // 两个指针, 一个i指向s中的字符, 一个j指向p中的字符

        // 明确递归函数的含义
        // digui(s, i, p, j) 表示s中的从i开始的所有字符, 能否被p中从j开始的所有字符匹配, 匹配x为true
        return digui(s, 0, p, 0);
    }

    public boolean digui(String s, int i, String p, int j) {
        
        if (visited.containsKey(i + ":" + j)) {
            return visited.get(i + ":" + j);
        }
        
        // base case
        // base case
        if (i == s.length()) {
            // p中如果还剩下字符, 那么必须是和*成对出现的才可以
            while (j < p.length()) {
                if (j + 1 < p.length() && p.charAt(j + 1) == '*') {
                    j += 2;
                } else {
                    return Boolean.FALSE;
                }
            }
            return Boolean.TRUE;
        }
        if (j == p.length()) {
            // 匹配字符串走到末尾的时候, 待匹配字符串是否匹配完毕了
            return i == s.length();
        }

        boolean ans;
        if (s.charAt(i) == p.charAt(j) || p.charAt(j) == '.') {
            // p的下一个字符是不是*
            if (j + 1 < p.length() && p.charAt(j + 1) == '*') {
                // 如果是* 就可以匹配0次或者多次
                ans = digui(s, i + 1, p, j) || digui(s, i, p, j + 2);
            } else {
                // 当前匹配成功, 继续匹配下一个
                ans = digui(s, i + 1, p, j + 1);
            }
        } else {
            // p的下一个字符是不是*
            if (j + 1 < p.length() && p.charAt(j + 1) == '*') {
                // 如果是* 就可以是0次
                ans = digui(s, i , p, j + 2);
            } else {
                // 匹配失败
                ans = Boolean.FALSE;
            }
        }
        visited.put(i + ":" + j, ans);
        return ans;

    }
}

正确的原因

(1) 一定要注意base case 不能想当然.

高楼扔鸡蛋

题目

image.png

版本1 错误

    public int superEggDrop(int k, int n) {

        // 明确状态
        // k个鸡蛋, n层楼, 状态就是楼层数和鸡蛋数量

        // 明确递归的含义
        // 这里的当前问题不能由已经解决的子问题得到答案, 因此更像是加了备忘录的回溯算法
        // digui(k, n) 的含义就是, 有k枚鸡蛋, 在n层楼的时候, 最坏情况下需要扔几次

        return digui(k, n);

    }

    public int digui(int k, int n) {

        // base case
        if (k == 1) {
            // 如果只有一枚鸡蛋, 那么一定是在最后一层楼扔鸡蛋会破
            return n;
        }
        if (n == 0) {
            return 0;
        }

        // 枚举从哪一层楼开始扔
        int min = Integer.MAX_VALUE;
        for(int i = 0; i < n; i ++) {
            // 最差情况
            int max = Math.max(digui(k - 1, i), digui(k, n - i));
            // 最少的次数
            min = Math.min(min, max);
        }
        return min;
    }

错误的原因

(1) 在枚举从哪一层开始的时候, 从0开始枚举的, 应该从1开始枚举, 不然n - 0 一直是n就死循环了

(2) 当鸡蛋碎了的时候, 下一个楼层应该是i - 1, 而不是i

(3) 在选择了第i层扔的时候, max的结果要 + 1, 表示这一层扔的次数

(4) 没有采用备忘录

版本2 错误

    Map<String, Integer> visited = new HashMap<>();
    public int superEggDrop(int k, int n) {

        // 明确状态
        // k个鸡蛋, n层楼, 状态就是楼层数和鸡蛋数量

        // 明确递归的含义
        // 这里的当前问题不能由已经解决的子问题得到答案, 因此更像是加了备忘录的回溯算法
        // digui(k, n) 的含义就是, 有k枚鸡蛋, 在n层楼的时候, 最坏情况下需要扔几次

        return digui(k, n);

    }

    public int digui(int k, int n) {

        // base case
        if (k == 1) {
            // 如果只有一枚鸡蛋, 那么一定是在最后一层楼扔鸡蛋会破
            return n;
        }
        if (n == 0) {
            return 0;
        }
        if (visited.containsKey(k + ":" + n)) {
            return visited.get(k + ":" + n);
        }

        // 枚举从哪一层楼开始扔
        int min = Integer.MAX_VALUE;
        for(int i = 1; i <= n; i ++) {
            // 最差情况
            int max = Math.max(digui(k - 1, i - 1), digui(k, n - i)) + 1;
            // 最少的次数
            min = Math.min(min, max);
        }

        visited.put(k + ":" + n, min);
        return min;
    }

错误的原因

(1) 当楼层数过多的时候, 还是会超时

版本3 错误

    static Map<String, Integer>  visited = new HashMap<>();
    public static int superEggDrop(int k, int n) {

        // 明确状态
        // k个鸡蛋, n层楼, 状态就是楼层数和鸡蛋数量

        // 明确递归的含义
        // 这里的当前问题不能由已经解决的子问题得到答案, 因此更像是加了备忘录的回溯算法
        // digui(k, n) 的含义就是, 有k枚鸡蛋, 在n层楼的时候, 最坏情况下需要扔几次

        return digui(k, n);

    }

    public static int digui(int k, int n) {

        // base case
        if (k == 1) {
            // 如果只有一枚鸡蛋, 那么一定是在最后一层楼扔鸡蛋会破
            return n;
        }
        if (n == 0) {
            return 0;
        }
        if (visited.containsKey(k + ":" + n)) {
            return visited.get(k + ":" + n);
        }

        // 枚举从哪一层楼开始扔
        // 最差情况是digui(k - 1, i - 1), digui(k, n - i)两个函数中值较大的一个
        // 自变量是i, digui(k - 1, i - 1)随着i的增大而增大
        // digui(k, n - i)随着i的增大而减少
        // 因此两个函数会有一个交点, 此时的值就是我们要求的min
        // 那么现在我们只需要找到一个i, 使得digui(k - 1, i - 1) == digui(k, n - i)即可
        // 利用二分查找法, 在1....n中寻找i
        // 需要注意的是, 两条直线在n比较小的时候可能并不会真正相交, 因此这个思路只是指导我们如何加快搜索
        int left = 1;
        int right = n;
        int min = Integer.MAX_VALUE;
        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (digui(k - 1, mid - 1) == digui(k, n - mid)) {
                min = Math.min(min, digui(k, n - mid));
                break;
            }
            if (digui(k - 1, mid - 1) > digui(k, n - mid)) {
                min = Math.min(min, digui(k, n - mid));
                right = mid - 1;
            }
            if (digui(k - 1, mid - 1) < digui(k, n - mid)) {
                min = Math.min(min, digui(k - 1, mid - 1));
                left = mid + 1;
            }
        }
        visited.put(k + ":" + n, min);
        return min;
    }

错误的原因

(1) 两个函数在给定参数的情况下, 是不一定会相交的, 因此没有相等的情况下, 也需要计算一次min

(2) 两个函数不相等的情况下, 取的是函数的最大值, 不是最小值, 就是这里min = Math.min(min, digui(k, n - mid));

(3) 千万注意, 计算出最小值了, 记得 + 1, 算上当前的次数

版本4 正确

    static Map<String, Integer>  visited = new HashMap<>();
    public static int superEggDrop(int k, int n) {

        // 明确状态
        // k个鸡蛋, n层楼, 状态就是楼层数和鸡蛋数量

        // 明确递归的含义
        // 这里的当前问题不能由已经解决的子问题得到答案, 因此更像是加了备忘录的回溯算法
        // digui(k, n) 的含义就是, 有k枚鸡蛋, 在n层楼的时候, 最坏情况下需要扔几次

        return digui(k, n);

    }

    public static int digui(int k, int n) {

        // base case
        if (k == 1) {
            // 如果只有一枚鸡蛋, 那么一定是在最后一层楼扔鸡蛋会破
            return n;
        }
        if (n == 0) {
            return 0;
        }
        if (visited.containsKey(k + ":" + n)) {
            return visited.get(k + ":" + n);
        }

        // 枚举从哪一层楼开始扔
        // 最差情况是digui(k - 1, i - 1), digui(k, n - i)两个函数中值较大的一个
        // 自变量是i, digui(k - 1, i - 1)随着i的增大而增大
        // digui(k, n - i)随着i的增大而减少
        // 因此两个函数会有一个交点, 此时的值就是我们要求的min
        // 那么现在我们只需要找到一个i, 使得digui(k - 1, i - 1) == digui(k, n - i)即可
        // 利用二分查找法, 在1....n中寻找i
        // 需要注意的是, 两条直线在n比较小的时候可能并不会真正相交, 因此这个思路只是指导我们如何加快搜索
        int left = 1;
        int right = n;
        int min = Integer.MAX_VALUE;
        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (digui(k - 1, mid - 1) == digui(k, n - mid)) {
                min = Math.min(min, digui(k, n - mid));
                break;
            }
            if (digui(k - 1, mid - 1) > digui(k, n - mid)) {
                // 根据思路, 是要取大的值
                min = Math.min(min, digui(k - 1, mid - 1));
                right = mid - 1;
            }
            if (digui(k - 1, mid - 1) < digui(k, n - mid)) {
                // 根据思路, 是要取大的值
                min = Math.min(min, digui(k, n - mid));
                left = mid + 1;
            }
        }
        visited.put(k + ":" + n, min + 1);
        return min + 1;
    }

戳气球

题目

image.png

版本1

    // 戳气球
    public int maxCoins(int[] nums) {

        // 明确状态
        // 状态就是气球的范围,

        // 明确dp数组含义
        // dp[i][j] = x; 表示在i....j 之间戳破气球能够得到的最大硬币数目x
        // 不包括i气球和j气球
        // 需要填充首尾两个额外的气球
        int [] newNums = new int[nums.length + 2];
        for (int i = 0; i < newNums.length; i ++) {
            if (i == 0 || i == newNums.length - 1) {
                newNums[i] = 1;
            } else {
                newNums[i] = nums[i - 1];
            }
        }
        int [][] dp = new int[newNums.length][newNums.length];

        // base case
        // dp[i][i + 1] = 0; 已经满足

        // 枚举所有状态
        // 斜着遍历
        for (int l = 2; l < newNums.length; l ++) {
            for (int i = 0; i < newNums.length; i ++) {
                int j = i + l;
                if (j >= newNums.length) {
                    break;
                }
                // 枚举i...j 之间最后一个戳破的气球是哪个
                for (int k = i + 1; k < j; k ++) {
                    dp[i][j] = Math.max(dp[i][j], dp[i][k] + dp[k][j] + newNums[i] * newNums[j] * newNums[k]);
                }
            }
        }

        return dp[0][newNums.length - 1];


    }

01背包问题

题目

image.png

版本1 错误

    // 背包最大价值
    public int pacakageMaxValue(int w, int [] val, int [] wt) {
        if (w == 0 || val.length == 0) {
            return 0;
        }
        // w是背包重量, val是不同物品的价值, wt是物品对应的重量

        // 明确状态
        // 背包的容量, 选择的物品

        // 明确dp数组的含义
        // dp[i][w] = x, 表示选择前i种物品, 此时背包的容量是w, 里面的最大价值是x
        int [][] dp = new int[val.length + 1][w + 1];

        // base case
        // dp[0][w] = 0;  dp[i][0] = 0; 已经满足

        for(int i = 1; i <= val.length; i ++) {
            for (int j = 1; j <= w; j ++) {
                if (j - wt[i - 1] >= 0) {
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - wt[i - 1]] + val[i - 1]);

                } else {
                    dp[i][j] = dp[i - 1][j];
                }
            }
        }

        return dp[val.length][w];


    }

错误的原因

(1) 在选择将第i个物品放入背包时候, 应该用的是dp[i - 1][j - wt[i - 1]] + val[i - 1], 而不是dp[i][j - wt[i - 1]] + val[i - 1], 因为每个物品只有一个.

版本2

    // 背包最大价值
    public int pacakageMaxValue(int w, int [] val, int [] wt) {
        if (w == 0 || val.length == 0) {
            return 0;
        }
        // w是背包重量, val是不同物品的价值, wt是物品对应的重量

        // 明确状态
        // 背包的容量, 选择的物品

        // 明确dp数组的含义
        // dp[i][w] = x, 表示选择前i种物品, 此时背包的容量是w, 里面的最大价值是x
        int [][] dp = new int[val.length + 1][w + 1];

        // base case
        // dp[0][w] = 0;  dp[i][0] = 0; 已经满足

        for(int i = 1; i <= val.length; i ++) {
            for (int j = 1; j <= w; j ++) {
                if (j - wt[i - 1] >= 0) {
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - wt[i - 1]] + val[i - 1]);

                } else {
                    dp[i][j] = dp[i - 1][j];
                }
            }
        }

        return dp[val.length][w];


    }

分割等和子集合

题目

image.png

版本1 正确

    // 将一个数组中的元素, 分割成两个子集, 两个子集和相等.
    public boolean canPartition(int[] nums) {
        if (nums.length == 0) {
            return Boolean.TRUE;
        }

        // 问题转化
        // 分割两个子集, 且和相等, 等同于在数组中挑选几个数字, 这几个数字的和刚好是数组和的一半

        // 明确状态
        // 当前挑选的数字, 以及当前的数字和


        int sum = IntStream.of(nums).sum();
        if (sum % 2 != 0) {
            // 不可能分成两个整数集合
            return Boolean.FALSE;
        }
        sum = sum / 2;
        // 明确dp数组的含义
        // dp[i][j] = x, 表示在nums中, 挑选前i个数字, 刚好构成了j, 如果构成x为true
        boolean [][] dp = new boolean[nums.length + 1][sum + 1];

        // base case
        // dp[0][j] = false; dp[i][0] = true;
        for (int i = 1; i <= nums.length; i ++) {
            dp[i][0] = Boolean.TRUE;
        }

        // 枚举所有状态
        for(int i = 1; i <= nums.length; i ++) {
            for (int j = 1; j <= sum; j ++) {
                if (j - nums[i - 1] >= 0) {
                    dp[i][j] = dp[i - 1][j] || dp[i - 1][j - nums[i - 1]];

                } else {
                    dp[i][j] = dp[i - 1][j];
                }
            }
        }

        return dp[nums.length][sum];


    }

零钱兑换2

题目

image.png

版本1 错误

    public int change(int amount, int[] coins) {
        if (amount == 0) {
            return 1;
        }
        if (coins.length == 0) {
            return 0;
        }
        
        // 不同于将数组分割成两个子集的问题
        // 那里面数组中每个元素都只能选一次, 但是这里每个硬币的数量是无限的

        // 明确状态
        // 硬币的种类, 当前的金额

        // 明确dp数组的含义
        // dp[i][j] = x, 表示选择前i中硬币, 能够构成金额j的可能种数为x
        int [][] dp = new int[coins.length + 1][amount + 1];

        // base case
        // dp[0][j] = 0; dp[i][0] = 1;
        for (int i = 1; i <= coins.length; i ++) {
            dp[i][0] = 1;
        }

        // 枚举所有状态
        for (int i = 1; i <= coins.length; i ++) {
            for (int j = 1; j <= amount; j ++) {
                if (j - coins[i - 1] >= 0) {
                    dp[i][j] = dp[i - 1][j] + dp[i - 1][j - coins[i - 1]];
                } else {
                    dp[i][j] = dp[i - 1][j];
                }
            }
        }
        
        return dp[coins.length][amount];

    }

错误的原因

(1) dp[i - 1][j - coins[i - 1]] 如果每样硬币只有一个, 那么应该是这用写法, 但是每样硬币如果是无限个的话, 应该写成dp[i][j - coins[i - 1]], 就表示随着j的增大, 允许使用多次i

版本2 正确

    public int change(int amount, int[] coins) {
        if (amount == 0) {
            return 1;
        }
        if (coins.length == 0) {
            return 0;
        }

        // 不同于将数组分割成两个子集的问题
        // 那里面数组中每个元素都只能选一次, 但是这里每个硬币的数量是无限的

        // 明确状态
        // 硬币的种类, 当前的金额

        // 明确dp数组的含义
        // dp[i][j] = x, 表示选择前i中硬币, 能够构成金额j的可能种数为x
        int [][] dp = new int[coins.length + 1][amount + 1];

        // base case
        // dp[0][j] = 0; dp[i][0] = 1;
        for (int i = 1; i <= coins.length; i ++) {
            dp[i][0] = 1;
        }

        // 枚举所有状态
        for (int i = 1; i <= coins.length; i ++) {
            for (int j = 1; j <= amount; j ++) {
                if (j - coins[i - 1] >= 0) {
                    dp[i][j] = dp[i - 1][j] + dp[i][j - coins[i - 1]];
                } else {
                    dp[i][j] = dp[i - 1][j];
                }
            }
        }

        return dp[coins.length][amount];

    }

打家劫舍

题目

image.png

版本1 正确

    Map<Integer, Integer> visited = new HashMap<>();
    public int rob(int[] nums) {

        // 明确状态
        // 当前在哪间房屋


        // 明确dp函数
        // 当前的问题不能够由已经解决的子问题得到答案, 因此需要递归
        // x = digui(nums, i), 表示从第i间房屋开始, 能够得到的最大的金钱x.
        return digui(nums, 0);

    }

    public int digui(int [] nums, int i) {
        if (i >= nums.length) {
            return 0;
        }
        
        if (visited.containsKey(i)) {
            return visited.get(i);
        }

        int ans = Integer.MIN_VALUE;
        // 处于每个状态下, 都可以选择打劫还是不打劫
        // 选择打劫
        ans = Math.max(ans, digui(nums, i + 2) + nums[i]);
        
        // 选择不打劫
        ans = Math.max(ans, digui(nums, i + 1));

        visited.put(i, ans);
        return ans;

    }

打家劫舍2

题目

image.png

版本1 错误

    // 环形房屋, 加个标志位, 如果第一个房子打劫了, 最后一个房子就不要打劫
    boolean flag = Boolean.FALSE;
    Map<Integer, Integer> visited = new HashMap<>();
    public int rob(int[] nums) {
        
        return digui(nums, 0);

    }

    public int digui(int [] nums, int i) {
        if (i >= nums.length) {
            return 0;
        }
        
        if (i != nums.length - 1 && visited.containsKey(i)) {
            return visited.get(i);
        }
        
        
        int ans = Integer.MIN_VALUE;
        if(i == 0) {
            // 标记第一个房屋打劫了
            flag = Boolean.TRUE;
        }

        if (i != nums.length - 1 || !flag) {
            // 选择打劫
            ans = Math.max(ans, digui(nums, i + 2) + nums[i]);
            
        }
        
        if(i == 0) {
            // 撤销第一个房屋的标记
            flag = Boolean.FALSE;
        }
        
        // 选择不打劫
        ans = Math.max(ans, digui(nums, i + 1));

        visited.put(i, ans);
        return ans;
    }

错误的原因

(1) 如果nums只有一个元素的话, 这样第一个房屋和最后一个房屋就是同一个, 就没办法正确得到打劫第一个房屋的钱了, 因此要单独处理这种情况

版本2 错误

    // 环形房屋, 加个标志位, 如果第一个房子打劫了, 最后一个房子就不要打劫
    static boolean flag = Boolean.FALSE;
    static Map<Integer, Integer> visited = new HashMap<>();
    public static int rob(int[] nums) {
        if (nums.length == 1) {
            return nums[0];
        }



        return digui(nums, 0);

    }

    public static int digui(int [] nums, int i) {
        if (i >= nums.length) {
            return 0;
        }

        if (i != nums.length - 1 && visited.containsKey(i)) {
            return visited.get(i);
        }


        int ans = Integer.MIN_VALUE;
        if(i == 0) {
            // 标记第一个房屋打劫了
            flag = Boolean.TRUE;
        }

        if (i != nums.length - 1 || !flag) {
            // 选择打劫
            ans = Math.max(ans, digui(nums, i + 2) + nums[i]);

        }

        if(i == 0) {
            // 撤销第一个房屋的标记
            flag = Boolean.FALSE;
        }

        // 选择不打劫
        ans = Math.max(ans, digui(nums, i + 1));

        visited.put(i, ans);
        return ans;
    }

错误的原因

(1) 在标志位不同的情况, visited所有的结果都是无法复用的, 因此必须清空map, 重新计算

(2) 同时在使用map数据的时候, 也无需判断是否是最后一个了

版本3 正确

// 环形房屋, 加个标志位, 如果第一个房子打劫了, 最后一个房子就不要打劫
    boolean flag = Boolean.FALSE;
    Map<Integer, Integer> visited = new HashMap<>();
    public int rob(int[] nums) {
        if (nums.length == 1) {
            return nums[0];
        }



        return digui(nums, 0);

    }

    public int digui(int [] nums, int i) {
        if (i >= nums.length) {
            return 0;
        }

        if (visited.containsKey(i)) {
            return visited.get(i);
        }


        int ans = Integer.MIN_VALUE;
        if(i == 0) {
            // 标记第一个房屋打劫了
            flag = Boolean.TRUE;
        }

        if (i != nums.length - 1 || !flag) {
            // 选择打劫
            ans = Math.max(ans, digui(nums, i + 2) + nums[i]);

        }

        if(i == 0) {
            // 撤销第一个房屋的标记
            flag = Boolean.FALSE;
            visited = new HashMap<>();
        }

        // 选择不打劫
        ans = Math.max(ans, digui(nums, i + 1));

        visited.put(i, ans);
        return ans;
    }

打家劫舍3

题目

image.png

方案1 正确

// 树形打家劫舍
    Map<TreeNode, Integer> visited = new HashMap<>();
    public int rob(TreeNode root) {
        return digui(root);
    }

    public int digui(TreeNode root) {
        if (root == null) {
            return 0;
        }
        if (visited.containsKey(root)) {
            return visited.get(root);
        }
        
        int ans = 0;
        // 选择打劫
        int left = 0;
        if (root.left != null) {
            left = digui(root.left.left) + digui(root.left.right);
        }
        
        int right = 0;
        if (root.right != null) {
            right = digui(root.right.left) + digui(root.right.right);
        }
        
        ans = Math.max(ans, root.val + left + right);
        
        // 不选择打劫
        ans = Math.max(ans, digui(root.left) + digui(root.right));
        visited.put(root, ans);
        return ans;
    }