272最大化糖果美味值问题 困难 | 豆包MarsCode AI刷题

11 阅读6分钟

题意:

小C面临一个选择问题:她可以从编号为1到n的糖果中任意选择一些糖果作为奖励,但需要遵守一个限制规则:如果选择了编号为i的糖果,那么编号为i-1、i-2、i+1、i+2的糖果将不能被选择。每个糖果都有一个对应的美味值,小C希望所选糖果的美味值之和最大。 例如:当有7个糖果,编号为1到7,美味值分别为 3, 1, 2, 7, 10, 2, 4,小C可以选择编号为1、4、7的糖果,这样可以得到最大美味值之和为 3 + 7 + 4 = 14。

题解:

在这道题中,我们要帮助小C从编号为 1 到 nnn 的糖果中选择一些糖果,以最大化这些糖果的美味值之和。题目提供了一个约束条件:如果选择了编号为 iii 的糖果,那么编号为 i−1i-1i−1、i−2i-2i−2、i+1i+1i+1、i+2i+2i+2 的糖果都不能被选择。也就是说,选择糖果会影响相邻两个编号的可选择性。为了满足这个约束条件,我们可以通过动态规划来求解这个问题。

问题分析

  1. 目标:从糖果中选择一个子集,使得所选糖果的美味值之和最大化。
  2. 约束:不能选择相邻两个糖果,及距离为 2 的糖果,具体而言,选择编号为 iii 的糖果后,编号为 i−1i-1i−1、i−2i-2i−2、i+1i+1i+1、i+2i+2i+2 的糖果将不能再选。

这个约束可以简化为一个 间隔选择 问题。经典的间隔选择问题可以通过动态规划来解决。此类问题的关键在于为每个位置决定“选择”或“不选择”的最佳方案,依次构建出最大化的美味值。

解决思路

动态规划的基本思想是递归地定义状态,将原问题分解为子问题,以避免重复计算。具体步骤如下:

  1. 定义状态

    • 定义一个函数 dfs(i, pre, ppre),表示当前处理第 iii 个糖果时,考虑的状态:

      • pre\text{pre}pre:表示是否选择了编号为 i−1i-1i−1 的糖果。
      • ppre\text{ppre}ppre:表示是否选择了编号为 i−2i-2i−2 的糖果。
    • 这样,我们就能判断当前糖果是否可以被选中。

  2. 状态转移

    • 如果 pre\text{pre}pre 或 ppre\text{ppre}ppre 为 1,表示选择了第 i−1i-1i−1 或第 i−2i-2i−2 个糖果,此时我们不能选择当前的第 iii 个糖果,唯一的选择是跳过第 iii 个糖果,进入到第 i+1i+1i+1 个糖果的决策。

    • 如果 pre\text{pre}pre 和 ppre\text{ppre}ppre 都为 0,表示编号为 i−1i-1i−1 和 i−2i-2i−2 的糖果都没有被选择,此时我们有两个选择:

      1. 跳过当前的第 iii 个糖果,进入到第 i+1i+1i+1 个糖果的决策。
      2. 选择当前的第 iii 个糖果,此时第 i+1i+1i+1 和第 i+2i+2i+2 个糖果将不能被选择,因此我们递归地进入第 i+1i+1i+1 个糖果的决策,并且标记当前状态为选择了第 iii 个糖果。
  3. 初始状态与边界条件

    • 初始时,我们从第 0 个糖果开始(假设编号从 0 开始),preppre 都为 0,表示没有选择之前的糖果。
    • 当 iii 达到 nnn 时,表示已经遍历完所有糖果,此时返回 0 表示没有额外的美味值可以增加。
  4. 优化与记忆化

    • 通过 @lru_cache 装饰器对 dfs 函数进行记忆化,使得每种状态在第一次计算后将被缓存,避免重复计算。
from functools import lru_cache
def solution(n: int, a: list) -> int:
    fmax = lambda x , y : x if x > y else y
    @lru_cache 
    def dfs(i : int , pre : int , ppre : int) :
        if i == n :
            return 0 
        if pre or ppre:
            return dfs(i + 1 ,0 , pre)
        return fmax(dfs(i + 1 , 0 , pre) , dfs(i + 1 , 1 , pre) + a[i])
    return dfs(0 , 0 , 0)

动态规划转化思路

  1. 定义状态:用 dp[i] 表示在选择编号为 iii 的糖果时可以获得的最大美味值之和。

  2. 状态转移方程

    • 对于每一个糖果 iii,我们有两种选择:

      1. 不选糖果 iii:此时最大美味值和为 dp[i-1]
      2. 选糖果 iii:此时最大美味值和为 a[i] + dp[i-3](因为选择了 iii 后,我们不能选择 i−1i-1i−1 和 i−2i-2i−2,所以可以加上 dp[i-3])。
    • 因此状态转移方程为: dp[i]=max⁡(dp[i−1],a[i]+dp[i−3])dp[i] = \max(dp[i-1], a[i] + dp[i-3])dp[i]=max(dp[i−1],a[i]+dp[i−3])

  3. 边界条件

    • 当 i=0i = 0i=0 时,dp[0] = a[0]
    • 当 i=1i = 1i=1 时,dp[1] = \max(a[0], a[1])
    • 当 i=2i = 2i=2 时,dp[2] = \max(a[0] + a[2], a[1])

动态规划代码实现

以下是将递归代码转换为迭代写法的动态规划实现:

复制代码
def solution(n: int, a: list) -> int:
    if n == 1:
        return a[0]
    if n == 2:
        return max(a[0], a[1])
    if n == 3:
        return max(a[0] + a[2], a[1])

    # 初始化 dp 数组
    dp = [0] * n
    dp[0] = a[0]
    dp[1] = max(a[0], a[1])
    dp[2] = max(a[0] + a[2], a[1])

    # 填充 dp 数组
    for i in range(3, n):
        dp[i] = max(dp[i-1], a[i] + dp[i-3])

    # 返回最后一个位置的值,即最大美味值之和
    return dp[-1]

代码说明

  1. 边界条件处理:我们先处理 n=1n = 1n=1、n=2n = 2n=2 和 n=3n = 3n=3 的情况,以便后续动态规划计算时不越界。

  2. 初始化 dp 数组

    • dp[0] 表示只选择第 0 个糖果的最大美味值,即 a[0]
    • dp[1] 表示选择前两个糖果中美味值较大的那个,即 max(a[0], a[1])
    • dp[2] 表示选择不相邻的第 0 和第 2 个糖果的最大美味值,即 max(a[0] + a[2], a[1])
  3. 状态转移:从 i=3i = 3i=3 开始,我们根据状态转移方程 dp[i] = max(dp[i-1], a[i] + dp[i-3]) 逐步填充 dp 数组。

  4. 最终结果dp[-1] 保存了选择糖果的最大美味值之和。