算法-动态规划

一、理论基础

  1. DP:如果某一问题有很多重叠子问题,使用动态规划是最有效的。动态规划中每一个状态一定是由上一个状态推导出来的
  2. 方法论:
    1. 确定dp数组及其下标含义
    2. 确定递推公式
    3. 确定dp数组初始值
    4. 确定遍历顺序

二、基础题目

  1. 斐波那契数

    1. 题目
    2. 思路
    3. 代码
      class Solution {
          public int fib(int n) {
              if (n < 2) {
                  return n;
              }
              int[] dp = new int[n + 1];
              dp[1] = 1;
              for (int i = 2; i <= n; i++) {
                  dp[i] = dp[i - 1] + dp[i - 2];
              }
              return dp[n];
          }
      }
  2. 爬楼梯

    1. 题目
      1. 假设你正在爬楼梯。需要 n 阶你才能到达楼顶。每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?

    2. 思路
    3. 代码
      class Solution {
          public int climbStairs(int n) {
              if (n < 3) {
                  return n;
              }
              int[] dp = new int[n + 1];
              dp[1] = 1;
              dp[2] = 2;
              for (int i = 3; i <= n; i++) {
                  dp[i] = dp[i - 1] + dp[i - 2];
              }
              return dp[n];
          }
      }
  3. 使用最小花费爬楼梯

    1. 题目
      1. 给你一个整数数组 cost ,其中 cost[i] 是从楼梯第 i 个台阶向上爬需要支付的费用。一旦你支付此费用,即可选择向上爬一个或者两个台阶。
    2. 思路
    3. 代码
      class Solution {
          public int minCostClimbingStairs(int[] cost) {
              int[] dp = new int[cost.length + 1];
              for (int i = 2; i <= cost.length; i++) {
                  dp[i] = Math.min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);
              }
              return dp[cost.length];
          }
      }
  4. 不同路径

    1. 题目
      1. 一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。问总共有多少条不同的路径?

    2. 思路
    3. 代码
      class Solution {
          public int uniquePaths(int m, int n) {
              int[][] dp = new int[m][n];
              for (int i = 0; i < m; i++) {
                  dp[i][0] = 1;
              }
              for (int i = 0; i < n; i++) {
                  dp[0][i] = 1;
              }
              for (int i = 1; i < m; i++) {
                  for (int j = 1; j < n; j++) {
                      dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
                  }
              }
              return dp[m - 1][n - 1];
          }
      }
  5. 不同路径II

    1. 题目
      1. 一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish”)。现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?网格中的障碍物和空位置分别用 1 和 0 来表示。

    2. 思路
    3. 代码
      class Solution {
          public int uniquePathsWithObstacles(int[][] obstacleGrid) {
              int m = obstacleGrid.length;
              int n = obstacleGrid[0].length;
              int[][] dp = new int[m][n];
              for (int i = 0; i < m; i++) {
                  if (obstacleGrid[i][0] == 1) {
                      break;
                  }
                  dp[i][0] = 1;
              }
              for (int i = 0; i < n; i++) {
                  if (obstacleGrid[0][i] == 1) {
                      break;
                  }
                  dp[0][i] = 1;
              }
              for (int i = 1; i < m; i++) {
                  for (int j = 1; j < n; j++) {
                      if (obstacleGrid[i][j] == 1) {
                          continue;
                      }
                      dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
                  }
              }
              return dp[m - 1][n - 1];
          }
      }
  6. 整数拆分

    1. 题目
      1. 给定一个正整数 n ,将其拆分为 k 个 正整数 的和( k >= 2 ),并使这些整数的乘积最大化。返回 你可以获得的最大乘积 。

    2. 思路
    3. 代码
      class Solution {
          public int integerBreak(int n) {
              int[] dp = new int[n + 1];
              dp[1] = 1;
              for (int i = 2; i <= n; i++) {
                  for (int j = 1; j < i / 2 + 1; j++) {
                      //每次计算dp[i],取最大值
                      dp[i] = Math.max(dp[i], Math.max(j * dp[i - j], j * (i - j)));
                  }
              }
              return dp[n];
          }
      }
  7. 不同的二叉树

    1. 题目
      1. 给你一个整数 n ,求恰由 n 个节点组成且节点值从 1 到 n 互不相同的 二叉搜索树 有多少种?返回满足题意的二叉搜索树的种数。
    2. 思路
    3. 代码
      class Solution {
          public int numTrees(int n) {
              //dp[i] 以1 ... i为节点组成二叉树的种数;
              //f(j) 以j为节点二叉数的种数;
              //dp[i] = f(1) + f(2) + ... +f(i);
              //f(j) = dp[j - 1] * dp[i - j];
              int[] dp = new int[n + 1];
              dp[1] = 1;
              dp[0] = 1;
              for (int i = 2; i <= n; i++) {
                  for (int j = 1; j <= i; j++) {
                      dp[i] += dp[j - 1] * dp[i - j];
                  }
              }
              return dp[n];
          }
      }

三、背包问题

  1. 01背包
    1. 有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。
    2. class Solution {
          public int maxBags(int[] weight, int[] value, int bagSize) {
              //dp[i][j] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少。
              int[][] dp = new int[value.length][bagSize + 1];
              for (int i = weight[i]; i <= bagSize; i++) {
                  dp[0][i] = value[0];
              }
              for (int i = 1; i < value.length; i++) {
                  for (int j = 1; j <= bagSize; j++) {
                      if (j >= weight[i]) {
                          dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
                      } esle {
                          dp[i][j] = dp[i - 1][j];
                      }
                  }
              }
              return dp[value.length - 1][bagSize];
          }
      
          public int maxBags(int[] weight, int[] value) {
              int[] dp = new int[bagSize + 1];
              for (int i = 0; i < value.length; i++) {
                  for (int j = bagSize; j >= weight[i]; j--) {
                      dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]);
                  }
              }
              return dp[bagSize];
          }
      }
  2. 完全背包
    1. 有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品能用无数次,求解将哪些物品装入背包里物品价值总和最大。
    2. class Solution {
      
          public int maxBags(int[] weight, int[] value, int bagSize) {
              int[][] dp = new int[value.length][bagSize + 1];
              for (int i = weight[i]; i <= bagSize; i++) {
                  dp[0][i] = value[0];
              }
              for (int i = 1; i < value.length; i++) {
                  for (int j = 1; j <= bagSize; j++) {
                      if (j >= weight[i]) {
                          dp[i][j] = Math.max(dp[i -][j], dp[i][j - weight[i]] + value[i]);
                      } esle {
                          dp[i][j] = dp[i - 1][j];
                      }
                  }
              }
              return dp[value.length - 1][bagSize];
          }
      
          public int maxBags(int[] weight, int[] value) {
              int[] dp = new int[bagSize + 1];
              for (int i = 0; i < value.length; i++) {
                  for (int j = weight[i]; j <= bagSize; j++) {
                      dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]);
                  }
              }
              return dp[bagSize];
          }
      }
  3. 多重背包
    1. 有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品能用nums[i]次,求解将哪些物品装入背包里物品价值总和最大。
    2. class Solution {
          public int maxBags(int[] weight, int[] value, int[] nums, int bagSize) {
              int[] dp = new int[bagSize + 1];
              for (int i = 0; i < value.length; i++) {
                  for (int j = weight[i]; j <= bagSize; j++) {
                      for (int k = 0; k <= nums[k] && j >= k * weight[i]; k++) {
                          dp[j] = Math.max(dp[j], dp[j - k * weight[i]] + k * value[i]);
                      }
                  }
              }
              return dp[bagSize];
          }
      }
  4. 分割等和子集
    1. 题目
      1. 给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
    2. 思路
      1. 放入体积为sum / 2的背包最大价格为 sum / 2,则可以等分
    3. 代码
      class Solution {
          public boolean canPartition(int[] nums) {
              int sum = Arrays.stream(nums).sum();
              if (sum % 2 != 0) {
                  return false;
              }
              sum /= 2;
              int[] dp = new int[sum + 1];
              for (int i = 0; i < nums.length; i++) {
                  for (int j = sum; j >= nums[i]; j--) {
                      dp[j] = Math.max(dp[j], dp[j - nums[i]] + nums[i]);
                  }
              }
              return sum == dp[sum];
          }
      }
  5. 最后一块石头的重量II
    1. 题目
      1. 有一堆石头,用整数数组 stones 表示。其中 stones[i] 表示第 i 块石头的重量。每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x <= y。那么粉碎的可能结果如下:如果 x == y,那么两块石头都会被完全粉碎;如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x。最后,最多只会剩下一块 石头。返回此石头 最小的可能重量 。如果没有石头剩下,就返回 0

    2. 思路
      1. 量让石头分成重量相同的两堆,相撞之后剩下的石头最小
    3. 代码
      class Solution {
          public int lastStoneWeightII(int[] stones) {
              int sums = Arrays.stream(stones).sum();
              int sum = sums / 2;
              int[] dp = new int[sum + 1];
              for (int i = 0; i < stones.length; i++) {
                  for (int j = sum; j >= stones[i]; j--) {
                      dp[j] = Math.max(dp[j], dp[j - stones[i]] + stones[i]);
                  }
              }
              return sums - 2 * dp[sum];
          }
      }
  6. 目标和
    1. 题目
      1. 给你一个非负整数数组 nums 和一个整数 target 。向数组中的每个整数前添加 '+' 或 '-' ,然后串联起所有整数,可以构造一个 表达式。返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。

    2. 思路
      1. 装满容量为size的背包,有几种方法
    3. 代码
      class Solution {
          public int findTargetSumWays(int[] nums, int target) {
              int sum = Arrays.stream(nums).sum();
              if (sum < -target || sum < target || (sum + target) % 2 != 0) {
                  return 0;
              }
              int size = (sum + target) / 2;
              size = size > 0 ? size : -size;
              //dp[i]: 从数组中获取和i的方法数。
              int[] dp = new int[size + 1];
              dp[0] = 1;
              for (int i = 0; i < nums.length; i++) {
                  for (int j = size; j >= nums[i]; j--) {
                      dp[j] += dp[j - nums[i]];
                  }
              }
              return dp[size];
          }
      }
  7. 一和零
    1. 题目
      1. 给你一个二进制字符串数组 strs 和两个整数 m 和 n 。请你找出并返回 strs 的最大子集的长度,该子集中 最多 有 m 个 0 和 n 个 1 。

    2. 思路
      1. strs 数组里的元素就是物品,每个物品都是一个!而m 和 n相当于是一个背包,两个维度的背包

    3. 代码
      class Solution {
          public int findMaxForm(String[] strs, int m, int n) {
              int[][] dp = new int[m + 1][n + 1];
              for (String str : strs) {
                  int zero = 0;
                  int one = 0;
                  for (char ch : str.toCharArray()) {
                      if (ch == '0') {zero++;}
                      if (ch == '1') {one++;}
                  }
                  for (int i = m; i >= zero; i--) {
                      for (int j = n; j >= one; j--) {
                          dp[i][j] = Math.max(dp[i][j], dp[i - zero][j - one] + 1);
                      }
                  }
              }
              return dp[m][n];
          }
      }
  8. 零钱兑换II
    1. 题目
      1. 给你一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0 。假设每一种面额的硬币有无限个。 

    2. 思路
      1. dp[j]:凑成总金额j的组合数为dp[j]
      2. dp[j] 就是所有的dp[j - coins[i]](考虑coins[i]的情况)相加。
      代码
      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];
          }
      }
  9. 组合总和IV
    1. 题目
      1. 给你一个由 不同 整数组成的数组 nums ,和一个目标整数 target 。请你从 nums 中找出并返回总和为 target 的元素组合的个数。排列问题
    2. 思路
      1. 顺序不同
      2. 先遍历背包,在遍历物品
    3. 代码
      class Solution {
          public int combinationSum4(int[] nums, int target) {
              int[] dp = new int[target + 1];
              dp[0] = 1;
              for (int j = 0; j <= target; j++) {
                  for (int num : nums) {
                      if (j >= num) {
                          dp[j] += dp[j - num];
                      }
                  }
              }
              return dp[target];
          }
      }
  10. 爬楼梯(进阶)
    1. 题目
      1. 假设你正在爬楼梯。需要 n 阶你才能到达楼顶。每次你可以爬至多m (1 <= m < n)个台阶。你有多少种不同的方法可以爬到楼顶呢?

    2. 思路
      1. 排列问题
      2. 背包容量n,物品质量1...m,可取无数次
    3. 代码
      class Solution {
          public int climbStairs(int n) {
              if (n < 3) {
                  return n;
              }
              int m = 2;
              int[] dp = new int[n + 1];
              dp[0] = 1;
              for (int j = 0; j <= n; j++) {
                  for (int i = 1; i <= m; i++) {
                      if (j >= i) {
                          dp[j] += dp[j - i];
                      }
                  }
              }
              return dp[n];
          }
      }
  11. 零钱兑换
    1. 题目
      1. 给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。你可以认为每种硬币的数量是无限的。

    2. 思路
      1. dp[j]:凑足总额为j所需钱币的最少个数为dp[j]
      2. 所以dp[j] 要取所有 dp[j - coins[i]] + 1 中最小的。
    3. 代码
      class Solution {
          public int coinChange(int[] coins, int amount) {
              int[] dp = new int[amount + 1];
              Arrays.fill(dp, amount + 1);
              dp[0] = 0;
              for (int coin : coins) {
                  for (int j = coin; j <= amount; j++) {
                      dp[j] = Math.min(dp[j], dp[j - coin] + 1);
                  }
              }
              return dp[amount] == amount + 1 ? -1 : dp[amount];
          }
      }
  12. 完全平方数
    1. 题目
      1. 给你一个整数 n ,返回 和为 n 的完全平方数的最少数量 。
    2. 思路
      1. dp[j]:和为j的完全平方数的最少数量为dp[j]
      2. dp[j] 可以由dp[j - i * i]推出, dp[j - i * i] + 1 便可以凑成dp[j]。选择最小的dp[j]

    3. 代码
      class Solution {
          public int numSquares(int n) {
              if (n < 4) {
                  return n;
              }
              int[] dp = new int[n + 1];
              Arrays.fill(dp, n + 1);
              dp[0] = 0;
              for (int i = 1; i <= n / 2; i++) {
                  for (int j = i * i; j <= n; j++) {
                      dp[j] = Math.min(dp[j], dp[j - i * i] + 1);
                  }
              }
              return dp[n];
          }
      }
  13. 单词拆分
    1. 题目
      1. 给你一个字符串 s 和一个字符串列表 wordDict 作为字典。请你判断是否可以利用字典中出现的单词拼接出 s 。
    2. 思路
      1. dp[i] : 字符串长度为i的话,dp[i]为true,表示可以拆分为一个或多个在字典中出现的单词.

      2. 如果确定dp[j] 是true,且 [j, i] 这个区间的子串出现在字典里,那么dp[i]一定是true。(j < i )。

    3. 代码
      class Solution {
          public boolean wordBreak(String s, List wordDict) {
              //字典里随便取,拼成放到s的背包里
              boolean[] dp = new boolean[s.length() + 1];
              dp[0] = true;
              for (int j = 0; j <= s.length(); j++) {
                  for (String str : wordDict) {
                      if (j >= str.length() && dp[j - str.length()] && str.equals(s.substring(j - str.length(), j))) {
                          dp[j] = true;
                          break;
                      }
                  }
              }
              return dp[s.length()];
          }
      }

四、打家劫舍

  1. 打家劫舍
    1. 题目
      1. 你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。

    2. 思路
      1. dp[i]:考虑下标i(包括i)以内的房屋,最多可以偷窃的金额为dp[i]
      2. 如果偷第i房间,那么dp[i] = dp[i - 2] + nums[i] 

      3. 如果不偷第i房间,那么dp[i] = dp[i - 1]

      4. 取最大值

    3. 代码
      class Solution {
          public int rob(int[] nums) {
              if (nums.length < 2) {
                  return nums[0];
              }
              int[] dp = new int[nums.length];
              dp[0] = nums[0];
              dp[1] = Math.max(nums[0], nums[1]);
              for (int i = 2; i < nums.length; i++) {
                  dp[i] = Math.max(dp[i - 1], dp[i - 2] + nums[i]);
              }
              return dp[nums.length - 1];
          }
      }
  2. 打家劫舍II
    1. 题目
      1. 你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。

    2. 思路
      1. 去头去尾各偷一次,取最大值
    3. 代码
      class Solution {
          public int rob(int[] nums) {
              if (nums.length == 1) {
                  return nums[0];
              }
              if (nums.length == 2) {
                  return Math.max(nums[0], nums[1]);
              }
              return Math.max(rob(nums, 0, nums.length - 1), rob(nums, 1, nums.length));
          }
      
          public int rob(int[] nums, int start, int end) {
              int[] dp = new int[end];
              dp[start] = nums[start];
              dp[start + 1] = Math.max(nums[start], nums[start + 1]);
              for (int i = start + 2; i < end; i++) {
                  dp[i] = Math.max(dp[i - 1], dp[i - 2] + nums[i]);
              }
              return dp[end - 1];
          }
      }
  3. 打家劫舍III
    1. 题目
      1. 小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为 root 。除了 root 之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果 两个直接相连的房子在同一天晚上被打劫 ,房屋将自动报警。给定二叉树的 root 。返回 在不触动警报的情况下 ,小偷能够盗取的最高金额 。

    2. 思路
      1. 树形dp
      2. 后序遍历(后处理数据)
    3. 代码
      class Solution {
          public int rob(TreeNode root) {
              int[] res = dfs(root);
              return Math.max(res[0], res[1]);
          }
      
          public int[] dfs(TreeNode root) {
              if (root == null) {
                  return new int[] { 0, 0 };
              }
              int[] left = dfs(root.left);
              int[] right = dfs(root.right);
              int[] dp = new int[2];
              dp[1] = left[0] + right[0] + root.val;
              dp[0] = Math.max(left[0], left[1]) + Math.max(right[0], right[1]);
              return dp;
          }
      }

五、股票问题

  1. 买卖股票的最佳时机
    1. 题目
      1. 给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。

    2. 思路
      1. dp[i][0] 表示第i天持有股票所得最多现金,dp[i][1] 表示第i天不持有股票所得最多现金
    3. 代码
      class Solution {
          public int maxProfit(int[] prices) {
              // dp[i][0]:第i天不持有的最大利润
              // dp[i][1]:第i天持有的最大利润
              int[] dp = new int[2];
              dp[1] = -prices[0];
              for (int i = 1; i < prices.length; i++) {
                  dp[0] = Math.max(dp[0], dp[1] + prices[i]);
                  dp[1] = Math.max(dp[1], -prices[i]);
              }
              return dp[0];
          }
      }
  2. 买卖股票的最佳时机II
    1. 题目
      1. 给你一个整数数组 prices ,其中 prices[i] 表示某支股票第 i 天的价格。在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。返回 你能获得的 最大 利润 。

    2. 思路
    3. 代码
      class Solution {
          public int maxProfit(int[] prices) {
              int[] dp = new int[2];
              dp[1] = -prices[0];
              for (int i = 1; i < prices.length; i++) {
                  dp[0] = Math.max(dp[0], dp[1] + prices[i]);
                  dp[1] = Math.max(dp[1], dp[0] - prices[i]);
              }
              return dp[0];
          }
      }
  3. 买卖股票的最佳时机III
    1. 题目
      1. 给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

    2. 思路
    3. 代码
      class Solution {
          public int maxProfit(int[] prices) {
              // dp[0]:不持有,dp[1]:第一次持有,dp[2]:第二次不持有,dp[3]:第二次持有,dp[4]:第二次不持有
              int[] dp = new int[5];
              dp[1] = -prices[0];
              dp[3] = -prices[0];
              for (int i = 1; i < prices.length; i++) {
                  dp[1] = Math.max(dp[1], dp[0] - prices[i]);
                  dp[2] = Math.max(dp[2], dp[1] + prices[i]);
                  dp[3] = Math.max(dp[3], dp[2] - prices[i]);
                  dp[4] = Math.max(dp[4], dp[3] + prices[i]);
              }
              return Math.max(dp[2], dp[4]);
          }
      }
  4. 买卖股票的最佳时机IV
    1. 题目
      1. 给你一个整数数组 prices 和一个整数 k ,其中 prices[i] 是某支给定的股票在第 i 天的价格。设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。也就是说,你最多可以买 k 次,卖 k 次。

    2. 思路
    3. 代码
      class Solution {
          public int maxProfit(int k, int[] prices) {
              int[] dp = new int[2 * k + 1];
              for (int i = 1; i <= 2 * k; i += 2) {
                  dp[i] = -prices[0];
              }
              for (int i = 1; i < prices.length; i++) {
                  for (int j = 1; j <= 2 * k; j += 2) {
                      dp[j] = Math.max(dp[j], dp[j - 1] - prices[i]);
                      dp[j + 1] = Math.max(dp[j + 1], dp[j] + prices[i]);
                  }
              }
              return dp[2 * k];
          }
      }
  5. 最佳买卖股票时机含冷冻期
    1. 题目
      1. 给定一个整数数组prices,其中第  prices[i] 表示第 i 天的股票价格 。​设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。
    2. 思路
    3. 代码
      class Solution {
          public int maxProfit(int[] prices) {
              // dp[i][0]:第i天不持有股票,dp[i][1]:第i天持有股票
              if (prices.length < 2) {
                  return 0;
              }
              int[][] dp = new int[prices.length][2];
              dp[0][1] = -prices[0];
              dp[1][0] = Math.max(dp[0][0], dp[0][1] + prices[1]);
              dp[1][1] = Math.max(dp[0][1], -prices[1]);
              for (int i = 2; 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 - 2][0] - prices[i]);
              }
              return dp[prices.length - 1][0];
          }
      }
  6. 买卖股票的最佳时期含手续费
    1. 题目
      1. 给定一个整数数组 prices,其中 prices[i]表示第 i 天的股票价格 ;整数 fee 代表了交易股票的手续费用。你可以无限次地完成交易,但是你每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。返回获得利润的最大值。注意:这里的一笔交易指买入持有并卖出股票的整个过程,每笔交易你只需要为支付一次手续费。

    2. 思路
    3. 代码
      class Solution {
          public int maxProfit(int[] prices, int fee) {
              // 买入的时候扣手续费
              int[] dp = new int[2];
              dp[1] = -prices[0] - fee;
              for (int i = 1; i < prices.length; i++) {
                  dp[1] = Math.max(dp[1], dp[0] - prices[i] - fee);
                  dp[0] = Math.max(dp[0], dp[1] + prices[i]);
              }
              return Math.max(dp[1], dp[0]);
          }
      }

六、子序列问题

  1. 最长递增子序列
    1. 题目
      1. 给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
    2. 思路
    3. 代码
      class Solution {
          public int lengthOfLIS(int[] nums) {
              // 1、dp[i]:以nums[i]为结尾最长递增子序列的长度
              int[] dp = new int[nums.length];
              Arrays.fill(dp, 1);
              int res = 1;
              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);
                      }
                  }
                  res = Math.max(res, dp[i]);
              }
              return res;
          }
      }
  2. 最长连续递增子序列
    1. 题目
      1. 给定一个未经排序的整数数组,找到最长且 连续递增的子序列,并返回该序列的长度。
    2. 思路
    3. 代码
      class Solution {
          public int findLengthOfLCIS(int[] nums) {
              int[] dp = new int[nums.length];
              Arrays.fill(dp, 1);
              int res = 1;
              for (int i = 1; i < nums.length; i++) {
                  if (nums[i] > nums[i - 1]) {
                      dp[i] = dp[i - 1] + 1;
                  }
                  res = Math.max(res, dp[i]);
              }
              return res;
          }
      }
  3. 最长重复子数组
    1. 题目
      1. 给两个整数数组 nums1 和 nums2 ,返回 两个数组中 公共的 、长度最长的子数组的长度 
    2. 思路
    3. 代码
      class Solution {
          public int findLength(int[] nums1, int[] nums2) {
              // dp[i][j] nums1以i-1为结尾,num2以j-1为结尾的公共最长子数组长度
              int[][] dp = new int[nums1.length + 1][nums2.length + 1];
              int res = 0;
              for (int i = 1; i <= nums1.length; i++) {
                  for (int j = 1; j <= nums2.length; j++) {
                      if (nums1[i - 1] == nums2[j - 1]) {
                          dp[i][j] = dp[i - 1][j - 1] + 1;
                      }
                      res = Math.max(res, dp[i][j]);
                  }
              }
              return res;
          }
      }
  4. 最长公共子序列
    1. 题目
      1. 给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。
    2. 思路
      1. 如果text1[i - 1] 与 text2[j - 1]相同,那么找到了一个公共元素,所以dp[i][j] = dp[i - 1][j - 1] + 1;

      2. 如果text1[i - 1] 与 text2[j - 1]不相同,那就看看text1[0, i - 2]与text2[0, j - 1]的最长公共子序列 和 text1[0, i - 1]与text2[0, j - 2]的最长公共子序列,取最大的。

    3. 代码
      class Solution {
          public int longestCommonSubsequence(String text1, String text2) {
              // dp[i][j]:以text1[i - 1],text2[j - 1]为结尾,最长公共子序列长度
              int[][] dp = new int[text1.length() + 1][text2.length() + 1];
              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()];
          }
      }
  5. 不相交的线
    1. 题目
    2. 思路
      1. 同寻找最大公共子序列
    3. 代码
      class Solution {
          public int maxUncrossedLines(int[] nums1, int[] nums2) {
              // dp[i][j]:以text1[i - 1],text2[j - 1]为结尾,最长公共子序列长度
              int[][] dp = new int[nums1.length + 1][nums2.length + 1];
              for (int i = 1; i <= nums1.length; i++) {
                  for (int j = 1; j <= nums2.length; j++) {
                      if (nums1[i - 1] == nums2[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[nums1.length][nums2.length];
          }
      }
  6. 最大子序和
    1. 题目
      1. 给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
    2. 思路
    3. 代码
      class Solution {
          public int maxSubArray(int[] nums) {
              int[] dp = new int[nums.length + 1];
              int res = nums[0];
              dp[0] = nums[0];
              for (int i = 1; i < nums.length; i++) {
                  if (dp[i - 1] > 0) {
                      dp[i] = dp[i - 1] + nums[i];
                  } else {
                      dp[i] = nums[i];
                  }
                  res = Math.max(res, dp[i]);
              }
              return res;
          }
      }
  7. 判断子序列
    1. 题目
      1. 给定字符串 s 和 t ,判断 s 是否为 t 的子序列。
    2. 思路
    3. 代码
      class Solution {
          public boolean isSubsequence(String s, String t) {
              // dp[i][j] 表示以下标i-1为结尾的字符串s,和以下标j-1为结尾的字符串t,相同子序列的长度为dp[i][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];
                      }
                  }
              }
              return dp[s.length()][t.length()] == s.length();
          }
      
          public boolean isSubsequence(String s, String t) {
              if (s.length() == 0) {
                  return true;
              }
              int index = 0;
              for (char ch : t.toCharArray()) {
                  if (s.charAt(index) == ch) {
                      index++;
                  }
                  if (index == s.length()) {
                      return true;
                  }
              }
              return index == s.length();
          }
      }
  8. 不同的子序列
    1. 题目
      1. 给你两个字符串 s 和 t ,统计并返回在 s 的 子序列 中 t 出现的个数,结果需要对 109 + 7 取模。        
    2. 思路
      1. s[i - 1] 与 t[j - 1]相等
        1. 一部分是用s[i - 1]来匹配,那么个数为dp[i - 1][j - 1]。即不需要考虑当前s子串和t子串的最后一位字母,所以只需要 dp[i-1][j-1]

        2. 一部分是不用s[i - 1]来匹配,个数为dp[i - 1][j]

      2. s[i - 1] 与 t[j - 1]不相等
        1. 不用s[i - 1]来匹配,个数为dp[i - 1][j]
    3. 代码
      class Solution {
          public int numDistinct(String s, String t) {
              // dp[i][j]:以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(); 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][j] = dp[i - 1][j - 1] + dp[i - 1][j];
                      } else {
                          dp[i][j] = dp[i - 1][j];
                      }
                  }
              }
              return dp[s.length()][t.length()];
          }
      }
  9. 两个字符串的删除操作
    1. 题目
      1. 给定两个单词 word1 和 word2 ,返回使得 word1 和  word2 相同所需的最小步数
    2. 思路
      1. 当word1[i - 1] 与 word2[j - 1]相同的时候,dp[i][j] = dp[i - 1][j - 1];
      2. 当word1[i - 1] 与 word2[j - 1]不相同的时候,取最小值
        1. 删word1[i - 1],最少操作次数为dp[i - 1][j] + 1
        2. 删word2[j - 1],最少操作次数为dp[i][j - 1] + 1
        3. 同时删word1[i - 1]和word2[j - 1],操作的最少次数为dp[i - 1][j - 1] + 2
    3. 代码
      class Solution {
          public int minDistance(String word1, String word2) {
              int[][] dp = new int[word1.length() + 1][word2.length() + 1];
              for (int i = 0; i <= word1.length(); i++) {
                  dp[i][0] = i;
              }
              for (int j = 0; j <= word2.length(); j++) {
                  dp[0][j] = j;
              }
              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 {
                          dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1);
                      }
                  }
              }
              return dp[word1.length()][word2.length()];
          }
      }
  10. 编辑距离
    1. 题目
      1. 给你两个单词 word1 和 word2, 请返回将 word1 转换成 word2 所使用的最少操作数  。你可以对一个单词进行如下三种操作:插入一个字符;删除一个字符;替换一个字符

    2. 思路
      1. 当word1[i - 1] 与 word2[j - 1]相同的时候,dp[i][j] = dp[i - 1][j - 1];​​​​​​​
      2. 当word1[i - 1] 与 word2[j - 1]不相同的时候,取最小值
          1. 删word1[i - 1],最少操作次数为dp[i - 1][j] + 1
          2. 删word2[j - 1],最少操作次数为dp[i][j - 1] + 1
        1. 增-删的逆向,操作次数相同
        2. 变-只需要一次替换的操作,就可以让 word1[i - 1] 和 word2[j - 1] 相同。
    3. 代码
      class Solution {
          public int minDistance(String word1, String word2) {
              int[][] dp = new int[word1.length() + 1][word2.length() + 1];
              for (int i = 0; i <= word1.length(); i++) {
                  dp[i][0] = i; 
              }
              for (int j = 0; j <= word2.length(); j++) {
                  dp[0][j] = j; 
              }
              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 {
                          dp[i][j] = Math.min(dp[i - 1][j - 1], Math.min(dp[i - 1][j], dp[i][j - 1])) + 1;
                      }
                  }
              }
              return dp[word1.length()][word2.length()];
          }
      }
  11. 回文字串​​​​​​​
    1. 题目
      1. 给你一个字符串 s ,请你统计并返回这个字符串中 回文子串 的数目。回文字符串 是正着读和倒过来读一样的字符串。子字符串 是字符串中的由连续字符组成的一个序列。

    2. 思路
      1. 布尔类型的dp[i][j]:表示区间范围[i,j] (注意是左闭右闭)的子串是否是回文子串
    3. 代码
      class Solution {
          public int countSubstrings(String s) {
              //dp[i][j]:表示区间范围[i,j] (注意是左闭右闭)的子串是否是回文子串
              boolean[][] dp = new boolean[s.length()][s.length()];
              int res = 0;
              for (int i = s.length() - 1; i >= 0; i--) {
                  for (int j = i; j < s.length(); j++) {
                      if (s.charAt(j) == s.charAt(i) && (j - i <= 1 || dp[i + 1][j - 1])) {
                          dp[i][j] = true; 
                          res++;   
                      }         
                  }
              }
              return res;
          }
      }
  12. 最长回文子序列​​​​​​​
    1. 题目
      1. 给你一个字符串 s ,找出其中最长的回文子序列,并返回该序列的长度。
    2. 思路
      1. s[i]与s[j]不相同,说明s[i]和s[j]的同时加入 并不能增加[i,j]区间回文子序列的长度。那么分别加入s[i]、s[j]看看哪一个可以组成最长的回文子序列。
    3. 代码
      class Solution {
          public int longestPalindromeSubseq(String s) {
              // dp[i][j] [i, j]范围内的最长回文子串
              int[][] dp = new int[s.length()][s.length()];
              for (int i = s.length() - 1; i >= 0; i--) {
                  for (int j = i; j < s.length(); j++) {
                      if (s.charAt(i) == s.charAt(j)) {
                          if (j - i <= 1) {
                              dp[i][j] = j - i + 1;
                          } else {
                              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. 确定dp数组及其下标的含义
  2. 确定递推公式
  3. 确定dp数组初始化
  4. 确定遍历顺序

未完待续......

你可能感兴趣的:(算法,算法,java,动态规划)