LeeCode 动态规划(四)

LeeCode 动态规划(四)

LeeCode 198:打家劫舍

题目描述

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警

给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。

建立模型

  1. 确定 dp 数组及下标的含义,数组的含义为在下标 0~i 的房屋内,能偷窃的最高金额
  2. 初始化 dp 数组:dp[0] = nums[0], dp[1] = Math.max(nums[0], nums[1])
  3. 确定递推公式:dp[i] = Math.max(dp[i - 2] + nums[i], dp[i - 1]) (2 <= i <= n - 1)
  4. 确定遍历顺序,i -> 2 ~ n - 1

代码实现

public int rob(int[] nums) {
  int n = nums.length;

  if (n == 1) {
    return nums[0];
  }

  if (n == 2) {
    return Math.max(nums[0], nums[1]);
  }

  int[] dp = new int[n];

  dp[0] = nums[0];
  dp[1] = Math.max(nums[0], nums[1]);

  for (int i = 2; i < n; i++) {
    dp[i] = Math.max(dp[i - 2] + nums[i], dp[i - 1]);
  }

  return dp[n - 1];
}

LeeCode 213:打家劫舍II

题目描述

你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。

给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。

建立模型

本题与 LeeCode 198 打家劫舍 唯一的区别就是第一个房屋和最后一个房屋相连,不能同时偷取。所以可以同时考虑以下两种情况:

  • 第一个房屋 ~ 倒数第二个房屋
  • 第二个房屋 ~ 倒数第一个房屋

最后,取两者的最大值。

代码实现

public int robII(int[] nums) {
  int n = nums.length;

  if (n == 1) {
    return nums[0];
  }

  if (n == 2) {
    return Math.max(nums[0], nums[1]);
  }

  // 第一个房屋 ~ 倒数第二个房屋
  int[] dp1 = new int[n - 1];
  
  // 第二个房屋 ~ 倒数第一个房屋
  int[] dp2 = new int[n - 1];

  dp1[0] = nums[0];
  dp1[1] = Math.max(nums[0], nums[1]);
  dp2[0] = nums[1];
  dp2[1] = Math.max(nums[1], nums[2]);

  for (int i = 2; i < n - 1; i++) {
    dp1[i] = Math.max(dp1[i - 2] + nums[i], dp1[i - 1]);
    dp2[i] = Math.max(dp2[i - 2] + nums[i + 1], dp2[i - 1]);
  }

  return Math.max(dp1[n - 2], dp2[n - 2]);
}

LeeCode 337:打家劫舍III

题目描述

小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为 root 。

除了 root 之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果 两个直接相连的房子在同一天晚上被打劫 ,房屋将自动报警。

给定二叉树的 root 。返回 在不触动警报的情况下 ,小偷能够盗取的最高金额 。

建立模型

本题将前两题的链式房屋结构转换成了树形结构,但也可以考虑动态规划,只不过是在树上进行状态转移。

该问题描述等价于一棵二叉树,树上每个节点都有对应的权值,每个节点有两种状态(选择不与不选择),在不能同时选择相连的 “父子” 节点的情况下,选中的点最大权值和是多少。

首先需要明确该问题是后序遍历的,要先确定下层节点的值。对于树上的每一个节点 node ,有选中与不选中两种选择。使用哈希表 f 表示选择当前节点情况下,其子树上被选中的最大权值和,哈希表 g 表示不选择当前节点情况下,其子树上被选中的最大权值和。

递推公式:

\[\begin{cases} f(o) = o.val + Math.max(g(o.left),\ g(o.right)) \\ g(o) = Math.max(f(o.left),\ g(o.left)) + Math.max(f(o.right),\ g(o.right)) \end{cases} \]

代码实现

Map<TreeNode, Integer> f = new HashMap<>();
Map<TreeNode, Integer> g = new HashMap<>();

public int robIII(int[] nums) {
  dfs(root);
  return Math.max(f.getOrDefault(root, 0), g.getOrDefault(root, 0));
}

public void dfs(TreeNode node) {
  if (node == null) {
    return;
  }

  dfs(node.left);
  dfs(node.right);

  f.put(node, node.val + g.getOrDefault(node.left, 0) + g.getOrDefault(node.right, 0));
  g.put(node, Math.max(f.getOrDefault(node.left, 0), g.getOrDefault(node.left, 0)) + 
        Math.max(f.getOrDefault(node.right, 0), g.getOrDefault(node.right, 0)));
}

LeeCode 121:买卖股票的最佳时机

题目描述

给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。

你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。

返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0

建立模型

  1. 确定 dp 数组及下标的含义,数组的含义为第 i 天卖出能得到的最大利润
  2. 初始化 dp 数组,dp[i] = 0 (0 <= i < prices.length)
  3. 确定递推公式:dp[i] = prices[i] - buy
  4. 确定遍历顺序,i -> 0 ~ prices.length - 1

代码实现

public int maxProfit(int[] prices) {
  // 使用 buy 记录当天之前的最低买入价
  int buy = prices[0];
  int[] dp = new int[prices.length];

  for (int i = 1; i < prices.length; i++) {
    dp[i] = prices[i] - buy;
    buy = Math.min(buy, prices[i]);
  }

  return Arrays.stream(dp).max().getAsInt();
}

LeeCode 122:买卖股票的最佳时机II

题目描述

给你一个整数数组 prices ,其中 prices[i] 表示某支股票第 i 天的价格。

在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。

返回 你能获得的 最大 利润 。

建立模型

本题的最优解法是贪心选择,选择的策略是 只有今天的股价高于昨天,就交易,即将数组中所有递增的差值求和。该策略的有效性证明可以查看官方解答

如何使用动态规划解决本题呢?

  1. 确定 dp 数组及下标的含义,数组的含义为: dp[i][0] -> 当天未持有股票已获的最大利润dp[i][1] -> 当天持有股票已获的最大利润

  2. 初始化 dp 数组:dp[0][0] = 0, dp[0][1] = -prices[0]

  3. 确定递推公式:

    \[\begin{cases} 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]) \end{cases} \]

  4. 确定遍历顺序 i -> 1 ~ prices.length

代码实现

/**
 * 贪心选择
 */
public int maxProfitII(int[] prices) {
  int res = 0;
  for (int i = 1; i < prices.length; i++) {
    res += Math.max(0, prices[i] - prices[i - 1]);
  }

  return res;
}


/**
 * 动态规划
 */
public int maxProfitII(int[] prices) {
  if (prices.length < 2) {
    return 0;
  }

  int[][] dp = new int[prices.length][2];

  // 0 表示未持有股票, 1表示持有股票
  dp[0][0] = 0;
  dp[0][1] = -prices[0];

  for (int i = 1; i < prices.length; i++) {
    dp[i][0] = Math.max(dp[i - 1][1] + prices[i], dp[i - 1][0]);
    dp[i][1] = Math.max(dp[i - 1][0] - prices[i], dp[i - 1][1]);
  }

  return dp[prices.length - 1][0];
}


/**
 * 状态压缩,因为当前状态只与上一状态有关
 */
public int maxProfitII(int[] prices) {
  if (prices.length < 2) {
    return 0;
  }
  
  // dp[0] 表示当前未持有股票的最大利润
  // dp[1] 表示当前已持有股票的最大利润
  int[] dp = new int[2];
  
  dp[0] = 0;
  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];
}

LeeCode 123: 买卖股票的最佳时机III

题目描述

给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。

设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。

注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

建立模型

最多可以完成两笔交易,所以任意一天结束之后会处于以下五个状态中的一种:

  • 未操作
  • 第一次买操作
  • 第一次卖操作
  • 第二次买操作
  • 第二次卖操作

动态规划分析步骤

  1. 确定 dp 数组及下标函数,数组的含义为第 i 天结束后处于状态 j 的最大利润

  2. 初始化 dp 数组:

    \[\begin{cases} dp[0][0] = 0 \\ dp[0][1] = -prices[0] \\ dp[0][2] = 0 \\ dp[0][3] = -prices[i] \\ dp[0][4] = 0 \end{cases} \]

  3. 确定递推公式:

    \[\begin{cases} dp[i][0] = dp[i - 1][0] \\ dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i]) \\ dp[i][2] = Math.max(dp[i - 1][2], dp[i - 1][1] + prices[i]) \\ dp[i][3] = Math.max(dp[i - 1][3], dp[i - 1][2] - prices[i]) \\ dp[i][4] = Math.max(dp[i - 1][4], dp[i - 1][3] + prices[i]) \end{cases} \]

  4. 确定递推顺序:i -> 1 ~ prices.length - 1

代码实现

public int maxProfitIII(int[] prices) {
  int[][] dp = new int[prices.length][5];

  /**
   * 0 —— 未操作
   * 1 —— 第一次买入
   * 2 —— 第一次卖出
   * 3 —— 第二次买入
   * 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][1], dp[i - 1][0] - prices[i]);
    dp[i][2] = Math.max(dp[i - 1][2], dp[i - 1][1] + prices[i]);
    dp[i][3] = Math.max(dp[i - 1][3], dp[i - 1][2] - prices[i]);
    dp[i][4] = Math.max(dp[i - 1][4], dp[i - 1][3] + prices[i]);
  }

  return dp[prices.length - 1][4];
}


/**
 * 状态压缩,当前状态只和前一状态有关,且状态0不影响dp过程
 */
public int maxProfitIII(int[] prices) {
  if (prices.length < 2) {
    return 0;
  }
  
  int[] dp = new int[4];
  
  /**
   * 0 -> 第一次买入
   * 1 -> 第一次卖出
   * 2 -> 第二次买入
   * 3 -> 第二次卖出
   */
  dp[0] = -prices[0];
  dp[1] = 0;
  dp[2] = -prices[i];
  dp[3] = 0;
  
  for (int i = 1; i < prices.length; i++) {
    dp[0] = Math.max(dp[0], -prices[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]);
  }
  
  return dp[3];
}

posted @ 2022-09-24 21:30  ylyzty  阅读(19)  评论(0编辑  收藏  举报