区间DP

  • dp[L…R]表示在区间[L…R]范围内的方案数等

    516. 最长回文子序列

    给你一个字符串 s ,找出其中最长的回文子序列,并返回该序列的长度。 子序列定义为:不改变剩余字符顺序的情况下,删除某些字符或者不删除任何字符形成的一个序列。

  1. public int longestPalindromeSubseq(String s) {
  2. int n = s.length();
  3. // dp[i...j]表示在s[i...j]范围内最长回文子序列的长度
  4. int[][] dp = new int[n][n];
  5. // 填dp表
  6. dp[n - 1][n - 1] = 1;
  7. for (int i = 0; i < n - 1; i++) {
  8. dp[i][i] = 1;
  9. dp[i][i + 1] = s.charAt(i) == s.charAt(i + 1) ? 2 : 1;
  10. }
  11. // 填dp表中剩余的格子
  12. for (int i = n - 3; i >= 0; i--) {
  13. for (int j = i + 2; j < n; j++) {
  14. if (s.charAt(i) == s.charAt(j)) {
  15. dp[i][j] = 2 + dp[i + 1][j - 1];
  16. } else {
  17. dp[i][j] = Math.max(dp[i + 1][j], dp[i][j - 1]);
  18. }
  19. }
  20. }
  21. return dp[0][n - 1];
  22. }

5. 最长回文子串

  1. // 在s[L...R]范围内寻找最长回文子串
  2. public String longestPalindrome(String s) {
  3. if (s == null || s.length() == 0) {
  4. return s;
  5. }
  6. // 最长子串开始的位置以及跨越的长度
  7. int start = 0;
  8. int step = 1;
  9. int n = s.length();
  10. // dp[i][j]表示字符串在[i...j]范围内是否是回文字符串
  11. boolean[][] dp = new boolean[n][n];
  12. // base case:i==j时,dp为true
  13. for (int i = 0; i < n; i++) {
  14. dp[i][i] = true;
  15. }
  16. // base case,j==i+1时,看这两个字符是否相等
  17. for (int i = 0; i < n - 1; i++) {
  18. dp[i][i + 1] = s.charAt(i) == s.charAt(i + 1);
  19. if (dp[i][i + 1]) {
  20. start = i;
  21. step = 2;
  22. }
  23. }
  24. // 普遍情况:填dp表格
  25. for (int i = n - 3; i >= 0; i--) {
  26. for (int j = i + 2; j < n; j++) {
  27. // dp[i][j]是否为回文,需要dp[i + 1][j - 1]是回文,且i、j位置的字符相等
  28. dp[i][j] = dp[i + 1][j - 1] && s.charAt(i) == s.charAt(j);
  29. if (dp[i][j] && j - i + 1 > step) {
  30. start = i;
  31. step = j - i + 1;
  32. }
  33. }
  34. }
  35. return s.substring(start, start + step);
  36. }

树形DP

  • 树形动态规划与线性动态规划没有本质区别。
  • 其实是套在深度优先遍历里的动态规划(在DFS的过程中实现DP)
  • 子问题就是”一颗子树”,状态一般表示为”以x为根的子树”,决策就是”x的子节点”

    337. 打家劫舍 III

    小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为 root 。 除了 root 之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果 两个直接相连的房子在同一天晚上被打劫 ,房屋将自动报警。 给定二叉树的 root 。返回 在不触动警报的情况下 ,小偷能够盗取的最高金额 。

  1. class Solution {
  2. // dp存储的是每个节点在偷/不偷两种情况下能偷到的最大值
  3. HashMap<TreeNode, int[]> dp = new HashMap<>();
  4. public int rob(TreeNode root) {
  5. dp.put(null, new int[]{0, 0});
  6. dfs(root);
  7. return Math.max(dp.get(root)[0], dp.get(root)[1]);
  8. }
  9. private void dfs(TreeNode root) {
  10. if (root == null) return;
  11. dp.put(root, new int[2]);
  12. dfs(root.left);
  13. dfs(root.right);
  14. // 当root不偷的情况下,left和right偷或者不偷都可以
  15. dp.get(root)[0] = Math.max(dp.get(root.left)[0], dp.get(root.left)[1]) +
  16. Math.max(dp.get(root.right)[0], dp.get(root.right)[1]);
  17. // 当root偷的情况下,root的left和right就都不能偷了
  18. dp.get(root)[1] = dp.get(root.left)[0] + dp.get(root.right)[0] + root.val;
  19. }
  20. }

124. 二叉树中的最大路径和

  1. class Solution {
  2. int maxSum = Integer.MIN_VALUE;
  3. public int maxPathSum(TreeNode root) {
  4. dfs(root);
  5. return maxSum;
  6. }
  7. // 计算子树对路径和的贡献值
  8. private int dfs(TreeNode root) {
  9. // base case
  10. if (root == null) {
  11. return 0;
  12. }
  13. // 计算左右子树的贡献值,如果贡献值小于0,就不要
  14. int leftValue = Math.max(dfs(root.left), 0);
  15. int rightValue = Math.max(dfs(root.right), 0);
  16. // 更新maxSum
  17. maxSum = Math.max(maxSum, leftValue + rightValue + root.val);
  18. // 当前节点root的贡献值
  19. return Math.max(leftValue, rightValue) + root.val;
  20. }
  21. }

总结

  • 树形DP其实是一种思想,一般使用后续遍历的思想:对于节点root,先遍历左右子树,需要从左右子树中得到什么信息从而丰富root的答案。想要得到的信息就是状态,从左右子树的状态得到root的信息就是状态转移,base case就是需要考虑数的边界问题(叶子节点和一些特殊条件)