动态规划

EmiyaCC 于 2021-07-23 发布

动态规划

动态规划问题的一般形式就是求最值,所以解决动态规划的核心就是穷举。而穷举存在“重复子问题”,所以需要用到“备忘录”和“DP Table”。同时动态规划问题一般可以化成子问题,有“最优子结构”,通过这个子问题去一步步得到原问题的最值。

典中典–斐波那契数

class Solution {
    public int fib(int n) {
        if (n < 2) return n;
        int[] dp = new int[n + 1];
        dp[0] = 0;
        dp[1] = 1;
        for (int i = 2; i < n + 1; i ++) {
            dp[i] = dp[i - 1] + dp[i - 2];
        }
        return dp[n];
    }
}

动态规划五部曲:

  1. 确定dp数组以及下标的含义
  2. 确定递推公式
  3. dp数组如何初始化
  4. 确定遍历顺序:
  5. 举例推导dp数组

规律总结:

  1. 背包问题

    1. 0-1 背包:如果不状态压缩,for 循环先物品还是背包无所谓;如果压缩了状态,需要先物品再背包,同时背包容量倒叙,

      1. 分割等和子集最后一块石头的重量 II 都是记录背包容量为 j 时能放下的物品的最大容量,所以递推公式是 $dp[j] = max(dp[j], dp[j - num[i]] + num[i])$
      2. 目标和 是记录可以到达背包容量 j 时有多少种组合,所以递推公式是 $dp[j] = dp[j] + dp[j - num[i]]$​
    2. 完全背包:循环先物品再背包求组合数,先背包再物品求排列数

      1. 零钱兑换 II 就是只需要就组合数,所以循环先物品再背包

      2. 爬楼梯组合总和 Ⅳ 需要求求排列数,所以先循环背包再物品

        上面都是求和,所以递推公式是 $dp[j] = dp[j] + dp[j - num[i]]$

      3. 零钱兑换完全平方数单词拆分 这些都是不分顺序,只需要注意 dp数组 更新就行

    3. 多重背包:可以拆分成 0-1 背包

  2. 打家劫舍问题

    1. 打家劫舍 是最原始的打家劫舍问题,比较简单
    2. 打家劫舍 II 相较于上题,区别在于首尾只能选一个打劫,所有本题可化简成两个正常的打家劫舍问题,一个记录没有尾节点的结果,一个记录没有头节点的结果
    3. 打家劫舍 III 是一道入门的树形dp问题,每个节点有两个值记录抢不抢。第一题其实也可以用二维数组来定义dp,第二维度表示抢不抢的结果
  3. 股票问题(核心在于记录当天的不同状态)

    1. 只能买卖一次 状态:持有股票、不持有股票
    2. 可以买卖多次 状态:持有股票、不持有股票。与上一题不同的是记录持有股票时的值只能买一次是 -price[i],而本题是 dp[i-1][1] - price[i]
    3. 最多买卖两次 状态:无操作、第一次买入、第一次卖出、第二次买入、第二次卖出
    4. 可以买卖 K 次 状态:无操作,第 i 次买入、第 i 次卖出。该题是上题的普适版本,用 for 循环来循环的更新 dp
    5. 含冷冻期 状态:无操作(不是冷却期)、冷却期、买入、卖出
    6. 含手续费 状态:持有股票、不持有股票。本题比 可以买卖多次 多了一个判断,就是卖出时需要减一个 fee,没什么区别
  4. 子序列问题

    1. 子序列不连续
      1. 最长递增子序列
      2. 最长公共子序列不相交的线 本质是一样的题
    2. 子序列连续
      1. 最长连续递增序列
      2. 最长重复子数组
      3. 最大子序和
  5. 编辑距离

    1. 判断子序列
    2. 不同的子序列
    3. 两个字符串的删除操作
    4. 编辑距离
  6. 回文

    1. 回文子串
    2. 最长回文子序列

背包问题

背包问题分类:01背包(元素只可使用一次)、完全背包(元素可多次使用)

动态规划五部曲:

  1. 确定dp数组以及下标的含义
  2. 确定递推公式
  3. dp数组如何初始化
    1. 这里建议画个二维数组,看看应该怎么写 for 循环,可见 回文子串 这个问题
  4. 确定遍历顺序:
    1. 二维dp可以先物品再背包容量,或者相反
    2. 压缩后的一维dp注意:
      1. 先遍历物品,后遍历背包。如果先遍历背包,那么背包里面就只会放入一个物品
      2. 在遍历背包时必须倒叙遍历,保证物品只被放入一次
  5. 举例推导dp数组

代码随想录-dp详解

0-1 背包

// 分割等和子集,就是01背包,选n个值出来成一堆,使得两堆和相同
class Solution {
    public boolean canPartition(int[] nums) {
        int sum = 0;
        for (int num : nums) {
            sum += num;
        }
        if (sum % 2 != 0) return false;
        int mid = sum / 2;
        int[] dp = new int[mid + 1];
        dp[0] = 0;
        for (int i = 0; i < nums.length; i ++) {
            // 状态压缩,就必须要先物品,在背包容量,同时背包容量需要倒叙,从后往前。注意 j >= nums[i]
            for (int j = mid; j >= nums[i]; j --) {
                dp[j] = Math.max(dp[j], dp[j - nums[i]] + nums[i]);
            }
        }
        return dp[mid] == mid ? true : false;
    }
}
  1. 确定dp数组以及下标的含义:

    dp[j] 表示背包容量为 j 的情况下可放入的物品的和的最大值

  2. 确定递推公式

    dp[j] = max(dp[j], dp[j - nums[i]] + nums[i])

  3. dp数组如何初始化

    dp[0] = 0,表示容量为 0 的情况下可放入的物品的和的最大值为 0

// 无状态压缩版本
// dp[i][j] 表示从下标 0-i 的物品拿到重为 j 的背包中最大的价值
// 首先需要初始化,即对 dp[0][j] 进行初始化
// 接着是递推公式,dp[i][j] = Max(dp[i - 1][j], dp[i - 1][weight[i]] + value[i]),由于本题重和价值都是相同的,所以可用进行状态压缩
// 需要注意的是,对于 weight[i] > j 的情况,也需要补充完整,在压缩状态情况下,这步可以不用做
class Solution {
    public boolean canPartition(int[] nums) {
        int sum = 0;
        for (int num : nums) {
            sum += num;
        }
        if (sum % 2 != 0) return false;
        int mid = sum / 2;
        int[][] dp = new int[nums.length][mid + 1];
        for (int j = nums[0]; j < mid + 1; j ++) {
            dp[0][j] = nums[0];
        }
        for (int i = 1; i < nums.length; i ++) {
            for (int j = 0; j < mid + 1; j ++) {
                // 对 j < nums[i] 情况进行补充
                if (j < nums[i]) dp[i][j] = dp[i - 1][j];
                else {
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - nums[i]] + nums[i]);
                }
            }
        }
        return dp[nums.length - 1][mid] == mid ? true : false;
    }
}

最后一块石头的重量 II

// https://leetcode-cn.com/problems/last-stone-weight-ii/
// 基本和上题相同,分差不多的两堆
class Solution {
    public int lastStoneWeightII(int[] stones) {
        int sum = 0;
        for (int stone : stones) {
            sum += stone;
        }
        int target = sum / 2;
        int[] dp = new int[target + 1];
        dp[0] = 0;
        for (int i = 0; i < stones.length; i ++) {
            for (int j = target; j >= stones[i]; j --) {
                dp[j] = Math.max(dp[j], dp[j - stones[i]] + stones[i]);
            }
        }
        return sum - dp[target] - dp[target];
    }
}
  1. 确定dp数组以及下标的含义:

    dp[j] 表示背包容量为 j 的情况下可放入的物品的和的最大值

  2. 确定递推公式

    dp[j] = max(dp[j], dp[j - nums[i]] + nums[i])

  3. dp数组如何初始化

    dp[0] = 0,表示容量为 0 的情况下可放入的物品的和的最大值为 0

目标和

// https://leetcode-cn.com/problems/target-sum/
class Solution {
    public int findTargetSumWays(int[] nums, int target) {
        int sum = 0;
        for (int num : nums) {
            sum += num;
        }
        if (sum < target) return 0;
        if ((sum - target) % 2 != 0) return 0;
        int mid = (sum - target) / 2;
        int[] dp = new int[mid + 1];
        dp[0] = 1;
        for (int i = 0; i < nums.length; i ++) {
            for (int j = mid; j >= nums[i]; j --) {
                dp[j] += dp[j - nums[i]];
            }
        }
        return dp[mid];
    }
}
  1. 确定dp数组以及下标的含义:

    dp[j] 表示背包容量可以达到 j 的组合的个数

  2. 确定递推公式

    dp[j] += dp[j - nums[i]]

  3. dp数组如何初始化

    dp[0] = 1,表示背包容量可以达到 0 的情况只有 1 种

完全背包

// 代码对比
// 0-1 背包
for (int i = 0; i < weight.length; i ++) {
    // 0-1 背包,从后往前,确保每个物品只添加一次
    for (int j = bagWeight; j >= weight[i]; j --) { 
        dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
    }
}
// 完全背包
for (int i = 0; i < weight.length; i ++) {
    // 完全背包,从前往后,确保每个物品可以添加多次
    for (int j = weight[i]; j <= bagWeight; j ++) {
        dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
    }
}
// 如果求组合数就是外层for循环遍历物品,内层for遍历背包。
// 如果求排列数就是外层for遍历背包,内层for循环遍历物品

求组合:

零钱兑换 II

// https://leetcode-cn.com/problems/coin-change-2/
// 只需要求组合数,所以先物品,再背包
class Solution {
    public int change(int amount, int[] coins) {
        int[] dp = new int[amount + 1];
        dp[0] = 1;
        for (int i = 0; i < coins.length; i ++) {
            for (int j = coins[i]; j <= amount; j ++) {
                dp[j] += dp[j - coins[i]]; 
            }
        }
        return dp[amount];
    }
}

求排序:

组合总和 Ⅳ

// https://leetcode-cn.com/problems/combination-sum-iv/
// 需要求排序,先背包,后物品
class Solution {
    public int combinationSum4(int[] nums, int target) {
        int[] dp = new int[target + 1];
        dp[0] = 1;
        // 先背包,所以 j 从 1 开始,这时需在后面判断 j 是否大于等于 nums[i]
        for (int j = 1; j <= target; j ++) {
            for (int i = 0; i < nums.length; i ++) {
                // 这里加上判断,只有 j 大于等于 nums[i] 才可以更新dp
                if (j >= nums[i]) {
                    dp[j] += dp[j - nums[i]];   
                }
            }
        }
        return dp[target];
    }
}

爬楼梯

// https://leetcode-cn.com/problems/climbing-stairs/
// 本题需要求排序,所以先背包,后物品
class Solution {
    public int climbStairs(int n) {
        if (n == 1) return 1;
        int[] steps = new int[] {1, 2};
        int[] dp = new int[n + 1];
        dp[1] = 1;
        dp[2] = 2;
        for (int j = 3; j <= n; j ++) {
            for (int i = 0; i < steps.length; i ++) {
                if (j >= steps[i]) {
                    dp[j] += dp[j - steps[i]];
                }
            }
        }
        return dp[n];
    }
}

不分顺序:

零钱兑换

// https://leetcode-cn.com/problems/coin-change/
class Solution {
    public int coinChange(int[] coins, int amount) {
        int[] dp = new int[amount + 1];
        // 初始化 dp
        for (int i = 0; i < amount + 1; i ++) {
            dp[i] = Integer.MAX_VALUE;
        }
        dp[0] = 0;
        for (int i = 0; i < coins.length; i ++) {
            for (int j = coins[i]; j <= amount; j ++) {
                // 判断 dp[j - coins[i]] 有没有值,即 dp[j - coins[i]] 能否被数组组合成
                if (dp[j - coins[i]] != Integer.MAX_VALUE) {
                    dp[j] = Math.min(dp[j], dp[j - coins[i]] + 1);
                }
            }
        }
        return dp[amount] == Integer.MAX_VALUE ? -1 : dp[amount];
    }
}

完全平方数

// https://leetcode-cn.com/problems/perfect-squares/
// 跟上题差不多,注意 for 循环里面的条件
class Solution {
    public int numSquares(int n) {
        int[] dp = new int[n + 1];
        for (int i = 0; i <= n; i ++) {
            dp[i] = Integer.MAX_VALUE;
        }
        dp[0] = 0;
        for (int i = 1; i * i <= n; i ++) {
            for (int j = i * i; j <= n; j ++) {
                dp[j] = Math.min(dp[j], dp[j - i * i] + 1);
            }
        }
        return dp[n];
    }
}

单词拆分

// https://leetcode-cn.com/problems/word-break/
// dp[i] 代表字符串 s 的 i 位置之前都可以凑成
class Solution {
    public boolean wordBreak(String s, List<String> wordDict) {
        // 用 HashSet 保证 wordDict 里面值唯一
        Set<String> wordDictSet = new HashSet<>(wordDict);
        int len = s.length();
        boolean[] dp = new boolean[len + 1];
        dp[0] = true;
        for (int j = 1; j <= len; j ++) {
            for (int i = 0; i < j; i ++) {
                // 判断 wordDictSet 字符串 s 有没有 i 以后的字符
                if (dp[i] && wordDictSet.contains(s.substring(i, j))) {
                    dp[j] = true;
                    // 当可以组成就 break 跳出
                    break;
                }
            }
        }
        return dp[len];
    }
}

多重背包

物品编号 物品重量 物品价值 物品数量
0 1 15 2
1 3 20 3

可以化成 0-1 背包

物品编号 物品重量 物品价值 物品数量
0 1 15 1
0 1 15 1
1 3 20 1
1 3 20 1
1 3 20 1

之后按照 0-1 背包解题即可

打家劫舍

打家劫舍

// https://leetcode-cn.com/problems/house-robber/
class Solution {
    public int rob(int[] nums) {
        int len = nums.length;
        if (len == 0) return 0;
        if (len == 1) return nums[0];
        int[] dp = new int[len + 1];
        dp[0] = 0;
        dp[1] = nums[0];
        for (int i = 2; i <= len; i ++) {
            dp[i] = Math.max(dp[i - 1], dp[i - 2] + nums[i - 1]);
        }
        return dp[len];
    }
}

打家劫舍 II

// https://leetcode-cn.com/problems/house-robber-ii/
// 本题相较上题,多了一个条件,那就是首尾两个值不能同时得到,所以可以将 nums 分为分别包括首和尾的两个部分
class Solution {
    public int rob(int[] nums) {
        int len = nums.length;
        if (len == 0) return 0;
        if (len == 1) return nums[0];
        int[] dp1 = new int[len];
        int[] dp2 = new int[len];
        // 初始化 dp1 
        dp1[0] = 0;
        dp1[1] = nums[0];
        for (int i = 2; i < len; i ++) {
            dp1[i] = Math.max(dp1[i - 1], dp1[i - 2] + nums[i - 1]);
        }
        // 初始化 dp2
        dp2[0] = 0;
        dp2[1] = nums[1];
        for (int i = 2; i < len; i ++) {
            dp2[i] = Math.max(dp2[i - 1], dp2[i - 2] + nums[i]);
        }
        // 最后取两种条件下的最大值
        return Math.max(dp1[len - 1], dp2[len - 1]);
    }
}

打家劫舍 III

// https://leetcode-cn.com/problems/house-robber-iii/
// 树形 DP 的入门题,用一个长度为 2 的 int 数组维护抢或不抢两种状态下的该节点的最大值
class Solution {
    public int rob(TreeNode root) {
        int[] res = robTree(root);
        return Math.max(res[0], res[1]);
    }
    int[] robTree(TreeNode root) {
        if (root == null) return new int[] {0, 0};
        int[] left = robTree(root.left);
        int[] right = robTree(root.right);
        int res1 = root.val + left[0] + right[0];
        int res2 = Math.max(left[0], left[1]) + Math.max(right[0], right[1]);
        return new int[] {res2, res1};
    }
}
// 暴力解法 每个节点会被访问两次,会超时
class Solution {
	public int rob(TreeNode* root) {
        if (root == NULL) return 0;
        if (root->left == NULL && root->right == NULL) return root->val;
        // 偷父节点
        int val1 = root->val;
        if (root->left) val1 += rob(root->left->left) + rob(root->left->right); // 跳过root->left,相当于不考虑左孩子了
        if (root->right) val1 += rob(root->right->left) + rob(root->right->right); // 跳过root->right,相当于不考虑右孩子了
        // 不偷父节点
        int val2 = rob(root->left) + rob(root->right); // 考虑root的左右孩子
        return max(val1, val2);
        }
    }
// 带记忆版 每个节点只会访问一次
class Solution {
    Map<TreeNode, Integer> map = new HashMap<>();

    public int rob(TreeNode root) {
        if (root == null) return 0;
        if (root.left == null && root.right == null) return root.val;
        if (map.containsKey(root)) return map.get(root);
        // 偷父节点
        int res1 = root.val;
        if (root.left != null) 
            res1 += rob(root.left.left) + rob(root.left.right);
        if (root.right != null)
            res1 += rob(root.right.left) + rob(root.right.right);
        // 不偷父节点
        int res2 = rob(root.left) + rob(root.right);
        // 记录
        int res = Math.max(res1, res2);
        map.put(root, res);
        return res;
    }
}

股票问题

只能买卖一次

// https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock/
// 记录状态,只有持有股票和不持有股票两种状态
class Solution {
    public int maxProfit(int[] prices) {
        int[][] dp = new int[prices.length][2];
        // [0]-[1] 表示 持有股票、不持有股票
        dp[0][0] = -prices[0];
        dp[0][1] = 0;
        for (int i = 1; i < prices.length; i ++) {
            dp[i][0] = Math.max(dp[i - 1][0], -prices[i]);
            dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] + prices[i]);
        }
        return dp[prices.length - 1][1];
    }
}

按照逻辑解法(这种解法不无脑,比如只有的加手续费的就不能无脑复制粘贴):

class Solution {
    public int maxProfit(int[] prices) {
        int min = prices[0];
        int[] dp = new int[prices.length];
        dp[0] = 0;
        for (int i = 1; i < prices.length; i ++) {
            dp[i] = Math.max(dp[i - 1], prices[i] - min);
            if (prices[i] < min)
                min = prices[i];
        }
        return dp[prices.length - 1];
    }
}
// 由于整个循环只用到两个值可以简化,后续类似,不过用 dp 数组简单好理解
class Solution {
    public int maxProfit(int[] prices) {
        int min = prices[0];
        int res = 0;
        int prev = 0;
        for (int i = 1; i < prices.length; i ++) {
            res = Math.max(prev, prices[i] - min);
            if (prices[i] < min)
                min = prices[i];
            prev = res;
        }
        return res;
    }
}

可以买卖多次

// https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-ii/
// 记录状态,只有持有股票和不持有股票两种状态
class Solution {
    public int maxProfit(int[] prices) {
        int[][] dp = new int[prices.length][2];
        // [0]-[1] 表示 持有股票、不持有股票
        dp[0][0] = -prices[0];
        dp[0][1] = 0;
        for (int i = 1; i < prices.length; 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[prices.length - 1][1];
    }
}

按照逻辑解法:

// 与上题不同的是本题可以多次买卖,所以需要累加
class Solution {
    public int maxProfit(int[] prices) {
        int[] dp = new int[prices.length];
        dp[0] = 0;
        int tmp = prices[0];
        for (int i = 1; i < prices.length; i ++) {
            if (prices[i] > tmp) {
                dp[i] = dp[i - 1] + prices[i] - tmp;
            } else {
                dp[i] = dp[i - 1];
            }
            tmp = prices[i];
        }
        return dp[prices.length - 1];
    }
}

最多买卖两次

// https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-iii/
// 比之前的难,每个点一共有 5 种状态,判断到达这几种状态时最大的收入
class Solution {
    public int maxProfit(int[] prices) {
        if (prices.length == 0) return 0;
        int[][] dp = new int[prices.length][5];
        // 0-4 表示 无操作、第一次买入、第一次卖出、第二次买入、第二次卖出
        dp[0][0] = 0;
        dp[0][1] = -prices[0];
        dp[0][2] = 0;
        dp[0][3] = -prices[0];
        dp[0][4] = 0;
        for (int i = 1; i < prices.length; i ++) {
            dp[i][0] = dp[i - 1][0];
            dp[i][1] = Math.max(dp[i - 1][0] - prices[i], dp[i - 1][1]);
            dp[i][2] = Math.max(dp[i - 1][1] + prices[i], dp[i - 1][2]);
            dp[i][3] = Math.max(dp[i - 1][2] - prices[i], dp[i - 1][3]);
            dp[i][4] = Math.max(dp[i - 1][3] + prices[i], dp[i - 1][4]);
        }
        return dp[prices.length - 1][4];
    }
}

可以买卖 K 次

// https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-iv/
// 本题算是上题的普适版本,也是把所有状态列出,只不过用 for 循环处理
class Solution {
    public int maxProfit(int k, int[] prices) {
        if (prices.length == 0) return 0;
        int[][] dp = new int[prices.length][2 * k + 1];
        for (int j = 1; j < 2 * k + 1; j += 2)   
            dp[0][j] = -prices[0];
        for (int i = 1; i < prices.length; i ++) {
            dp[i][0] = dp[i - 1][0];
            for (int j = 1; j < 2 * k + 1; j += 2)   
                dp[i][j] = Math.max(dp[i - 1][j - 1] - prices[i], dp[i - 1][j]);
            for (int j = 2; j < 2 * k + 1; j += 2)
                dp[i][j] = Math.max(dp[i - 1][j - 1] + prices[i], dp[i - 1][j]);
        }
        return dp[prices.length - 1][2 * k];
    }
}

最佳买卖股票时机含冷冻期

// https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-with-cooldown/
// 本题也需要分状态,每一天对应四种状态,要分开讨论
class Solution {
    public int maxProfit(int[] prices) {
        int len = prices.length;
        int[][] dp = new int[len][4];
        // 0-3 表示 无操作(不是冷却期)、无操作(冷却期)、买入、卖出
        dp[0][0] = 0;
        dp[0][1] = 0;
        dp[0][2] = -prices[0];
        dp[0][3] = 0;
        for (int i = 1; i < len; i ++) {
            dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1]);
            dp[i][1] = dp[i - 1][3];
            // 买入状态可以保持
            dp[i][2] = Math.max(dp[i - 1][2], Math.max(dp[i - 1][0], dp[i - 1][1]) - prices[i]);
            // 卖出状态不可用保持,卖出后就进入冷却期状态
            dp[i][3] = dp[i - 1][2] + prices[i];
        }
        return Math.max(dp[len - 1][0], Math.max(dp[len - 1][1], dp[len - 1][3]));
    }
}

股票问题就是总结每天状态的问题,比如前面的 可以买卖多次 问题,根据总结状态这个解法可以得到

class Solution {
    public int maxProfit(int[] prices) {
        int len = prices.length;
        int[][] dp = new int[len][3];
        // 0-3 表示 无操作、买入、卖出
        dp[0][0] = 0;
        dp[0][1] = -prices[0];
        dp[0][2] = 0;
        for (int i = 1; i < len; i ++) {
            dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][2]);
            dp[i][1] = Math.max(dp[i - 1][1], Math.max(dp[i - 1][0], dp[i - 1][2]) - prices[i]);
            dp[i][2] = dp[i - 1][1] + prices[i];
        }
        return Math.max(dp[len - 1][0], dp[len - 1][2]);
    }
}

买卖股票的最佳时机含手续费

// https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-with-transaction-fee/
class Solution {
    public int maxProfit(int[] prices, int fee) {
        int[][] dp = new int[prices.length][2];
        // 两种状态,持有股票、不持有股票
        dp[0][0] = -prices[0];
        dp[0][1] = 0;
        for (int i = 1; i < prices.length; 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] - fee);
        }
        return dp[prices.length - 1][1];
    }
}

子序列问题

子序列不连续

最长递增子序列

// https://leetcode-cn.com/problems/longest-increasing-subsequence/
class Solution {
    public int lengthOfLIS(int[] nums) {
        if (nums.length <= 1) return nums.length;
        int[] dp = new int[nums.length];
        for (int i = 0; i < nums.length; i ++) 
            dp[i] = 1;
        int res = 0;
        for (int i = 1; i < nums.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;
    }
}

最长公共子序列

// https://leetcode-cn.com/problems/longest-common-subsequence/
class Solution {
    public int longestCommonSubsequence(String text1, String text2) {
        int[][] dp = new int[text1.length() + 1][text2.length() + 1];
        // 初始化 dp[0][...] 和 dp[...][0] 为 0
        for (int i = 1; i <= text1.length(); i ++) {
            char char1 = text1.charAt(i - 1);
            for (int j = 1; j <= text2.length(); j ++) {
                char char2 = text2.charAt(j - 1);
                if (char1 == char2) {
                    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()];
    }
}

不相交的线

// https://leetcode-cn.com/problems/uncrossed-lines/
// 跟上题,最长公共子序列一模一样
class Solution {
    public int maxUncrossedLines(int[] nums1, int[] nums2) {
        int[][] dp = new int[nums1.length + 1][nums2.length + 1];
        for (int i = 1; i <= nums1.length; i ++) {
            int tmp1 = nums1[i - 1];
            for (int j = 1; j <= nums2.length; j ++) {
                int tmp2 = nums2[j - 1];
                if (tmp1 == tmp2) {
                    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[nums1.length][nums2.length];
    }
}

子序列连续

最长连续递增序列

// https://leetcode-cn.com/problems/longest-continuous-increasing-subsequence/
// 本题与 最长递增子序列 区别在于,本题只需要比较连续的两个值是否递增,不递增就归 1;而最长递增子序列需要比较前面的每一个数,所有需要 for 循环
class Solution {
    public int findLengthOfLCIS(int[] nums) {
        if (nums.length == 0) return 0;
        int res = 1;
        int[] dp = new int[nums.length];
        for (int i = 0; i < nums.length; i ++) {
            dp[i] = 1;
        }
        for (int i = 1; i < nums.length; i ++) {
            if (nums[i] > nums[i - 1]) {
                dp[i] = dp[i - 1] + 1;
            }
            if (dp[i] > res) res = dp[i];
        }
        return res;
    }
}

进阶版的最长递增序列

// https://www.nowcoder.com/questionTerminal/b65b8b1376d94d4b8d4718ba2f4c0022?toCommentId=9612441
// 本题需要自己去映射一个新的数组
import java.util.*;
public class Main {
    /*
    更改题目   a=[D,B,C,A]; b=[A,B,C,D]
    对 b 做映射  A->0; B->1; C->2; D->3
    那么对应的 a 可根据映射规则更改为 a'=[3,1,2,0]
    之后对 a' 做最长连续上升子数组即可
    */
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        int[] a = new int[n];
        int[] b = new int[n];
        for (int i = 0; i < n; i ++) a[i] = sc.nextInt();
        for (int i = 0; i < n; i ++) b[i] = sc.nextInt();
        // 对 A 里面的数进行重新映射
        Map<Integer, Integer> map = new HashMap<>();
        for (int i = 0; i < n; i ++) map.put(b[i], i);
        // 利用映射规则对 B 进行更新映射
        for (int i = 0; i < n; i ++) a[i] = map.get(a[i]);
        // 对更改之后的 b 做最长连续上升子序列
        int res = 0;
        int cur = 1;
        for (int i = 1; i < n; i ++) {
            if (a[i]>a[i-1]) {
                cur += 1;
                res = Math.max(res, cur);
            } else cur = 1;
        }
        System.out.println(n - res);
    }
}

最长重复子数组

// https://leetcode-cn.com/problems/maximum-length-of-repeated-subarray/
// 本题与 最长公共子序列 的区别在于,当加入的两个值不相同,最长公共子序列 会选 dp[i - 1][j] 和 dp[i][j - 1] 的最大值;而本题是令其为 0
class Solution {
    public int findLength(int[] nums1, int[] nums2) {
        int[][] dp = new int[nums1.length + 1][nums2.length + 1];
        int res = 0;
        for (int i = 1; i <= nums1.length; i ++) {
            int tmp1 = nums1[i - 1];
            for (int j = 1; j <= nums2.length; j ++) {
                int tmp2 = nums2[j - 1];
                if (tmp1 == tmp2) {
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                    if (dp[i][j] > res)
                        res = dp[i][j];
                }
            }
        }
        return res;
    }
}

最大子序和

// https://leetcode-cn.com/problems/maximum-subarray/
// 本题 dp 的更新条件是选取(该点值,该点值+之前值之和)的最大值
class Solution {
    public int maxSubArray(int[] nums) {
        int[] dp = new int[nums.length];
        dp[0] = nums[0];
        int res = nums[0];
        for (int i = 1; i < nums.length; i ++) {
            dp[i] = Math.max(dp[i - 1] + nums[i], nums[i]);
            if (dp[i] > res) 
                res = dp[i];
        }
        return res;
    }
}

编辑距离

判断子序列

// https://leetcode-cn.com/problems/is-subsequence/
class Solution {
    public boolean isSubsequence(String s, String t) {
        // dp[i][j] 表示 s 中 i 位置前的与 t 中 j 位置前的字符相同的个数是多少
        int[][] dp = new int[s.length() + 1][t.length() + 1];
        for (int i = 1; i <= s.length(); i ++) {
            for (int j = 1; j <= t.length(); j ++) {
                if (s.charAt(i - 1) == t.charAt(j - 1)) {
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                } else {
                    // 新加入的字符不同时的递推公式
                    dp[i][j] = dp[i][j - 1];
                }
            }
        }
        if (dp[s.length()][t.length()] == s.length()) return true;
        return false;
    }
}

不同的子序列

// https://leetcode-cn.com/problems/distinct-subsequences/
// 当把问题抽象成距离问题,这题就比较模板化了,跟上题差不多,区别是新加入的两个字符相同时的更新规则;以及初始化规则
class Solution {
    public int numDistinct(String s, String t) {
        if (s.length() < t.length()) return 0;
        int[][] dp = new int[s.length() + 1][t.length() + 1];
        // 当 t 为空,怎么都有 1 种解法
        for (int i = 0; i <= s.length(); i ++) 
            dp[i][0] = 1;
        for (int i = 1; i <= s.length(); i ++) {
            for (int j = 1; j <= t.length(); j ++) {
                if (s.charAt(i - 1) == t.charAt(j - 1)) {
                    // 新加入的两个字符相同时,更新时需要加上的是 dp[i -1][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()];
    }
}

两个字符串的删除操作

// https://leetcode-cn.com/problems/delete-operation-for-two-strings/
// 一步到位,dp 代表相同需要删除多少步
class Solution {
    public int minDistance(String word1, String word2) {
        int[][] dp = new int[word1.length() + 1][word2.length() + 1];
        // 初始化 dp
        for (int i = 1; i <= word1.length(); i ++) 
            dp[i][0] = i;
        for (int j = 1; j <= word1.length(); j ++) 
            dp[0][j] = j;
        for (int i = 1; i <= word1.length(); i ++) {
            char tmp1 = word1.charAt(i - 1);
            for (int j = 1; j <= word2.length(); j ++) {
                char tmp2 = word2.charAt(j - 1);
                if (tmp1 == tmp2) 
                    dp[i][j] = dp[i - 1][j - 1];
                else {
                    dp[i][j] = Math.min(Math.min(dp[i - 1][j], dp[i][j - 1]) + 1, dp[i - 1][j - 1] + 2);
                }
            }
        }
        return dp[word1.length()][word2.length()];
    }   
}
// 先求最长公共子序列,然后再求删除多少步
class Solution {
    public int minDistance(String word1, String word2) {
        int[][] dp = new int[word1.length() + 1][word2.length() + 1];
        for (int i = 1; i <= word1.length(); i ++) {
            char tmp1 = word1.charAt(i - 1);
            for (int j = 1; j <= word2.length(); j ++) {
                char tmp2 = word2.charAt(j - 1);
                if (tmp1 == tmp2) {
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                } else {
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
                }
            }
        }
        return word1.length() + word2.length() - 2 * dp[word1.length()][word2.length()];
    }
}

编辑距离

// https://leetcode-cn.com/problems/edit-distance/
// 最难的编辑问题,可以增删改,定义 dp 为达到相同所需的操作。递推公式中,当新加入的两个字符一样,则 dp[i][j] = dp[i-1][j-1];如果新加入的两个字符不一样,那么可分为增删改三种情况,当中,对一个增相当于对另一个删,所以这两种可以归为一种情况,那么可归纳为 1.对 word1 增;2.对 word2 增;3.同时增,取这三种情况的最小值 + 1.
class Solution {
    public int minDistance(String word1, String word2) {
        int[][] dp = new int[word1.length() + 1][word2.length() + 1];
        // 初始化
        for (int i = 1; i <= word1.length(); i ++) 
            dp[i][0] = i;
        for (int j = 1; j <= word2.length(); j ++) 
            dp[0][j] = j;
        for (int i = 1; i <= word1.length(); i ++) {
            char tmp1 = word1.charAt(i - 1);
            for (int j = 1; j <= word2.length(); j ++) {
                char tmp2 = word2.charAt(j - 1);
                if (tmp1 == tmp2) {
                    dp[i][j] = dp[i - 1][j - 1];
                } else {
                    dp[i][j] = Math.min(Math.min(dp[i - 1][j], dp[i][j - 1]), dp[i - 1][j - 1]) + 1;
                }
            }
        }
        return dp[word1.length()][word2.length()];
    }
}

回文

// 定义 dp 为是否为回文子串
class Solution {
    public int countSubstrings(String s) {
        boolean[][] dp = new boolean[s.length()][s.length()];
        int res = 0;
        for (int j = 1; j < s.length(); j ++) {
            char tmp = s.charAt(j);
            for (int i = 0; i < j; i ++) {
                if (s.charAt(i) == tmp && (j - i <= 2 || dp[i + 1][j - 1])) {
                    dp[i][j] = true;
                    res ++;
                }
            }
        }
        return res;
    }
}
// 另外一种处理方式
// 回文子串的处理,首先注意 i 的起始位置,i 表示左边边界,所以从尾巴开始倒着循环
class Solution {
    public int countSubstrings(String s) {
        boolean[][] dp = new boolean[s.length()][s.length()];
        // 初始化,由于要统计 true 的个数,所以不初始化
        int res = 0;
        for (int i = s.length() - 1; i >= 0; i --) {
            for (int j = i; j < s.length(); j ++) {
                if (s.charAt(i) == s.charAt(j) && (j - i <= 1 || dp[i + 1][j - 1])) {
                        dp[i][j] = true;
                        res += 1;
                    }
                }
            }
        }
        return res;
    }
}

最长回文子序列

// https://leetcode-cn.com/problems/longest-palindromic-subsequence/
class Solution {
    public int longestPalindromeSubseq(String s) {
        int[][] dp = new int[s.length()][s.length()];
        // 初始化
        for (int i = 0; i < s.length(); i ++) 
            dp[i][i] = 1;
        for (int i = s.length() - 1; i >= 0; i --) {
            // 右边界不跟左边界重合       
            for (int j = i + 1; j < s.length(); j ++) {
                if (s.charAt(i) == s.charAt(j)) {
                    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];
    }
}
class Solution {
    public String longestPalindrome(String s) {
        int left = 0, right = 0, maxLen = 1;
        boolean[][] dp = new boolean[s.length()][s.length()];
        for (int j = 1; j < s.length(); j ++) {
            int tmp = s.charAt(j);
            for (int i = 0; i < j; i ++) {
                if (s.charAt(i) == tmp && (j - i <= 2 || dp[i + 1][j - 1])) {
                    dp[i][j] = true;
                    if (j - i + 1 > maxLen) {
                        left = i;
                        right = j;
                        maxLen = j - i + 1;
                    }
                }
            }
        }
        return s.substring(left, right + 1);
    }
}

补充

确定 dp 的推导函数之后,怎么遍历

dp循环方向

Reference:

  • https://github.com/youngyangyang04/leetcode-master