题意:
小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 的糖果都不能被选择。也就是说,选择糖果会影响相邻两个编号的可选择性。为了满足这个约束条件,我们可以通过动态规划来求解这个问题。
问题分析
- 目标:从糖果中选择一个子集,使得所选糖果的美味值之和最大化。
- 约束:不能选择相邻两个糖果,及距离为 2 的糖果,具体而言,选择编号为 iii 的糖果后,编号为 i−1i-1i−1、i−2i-2i−2、i+1i+1i+1、i+2i+2i+2 的糖果将不能再选。
这个约束可以简化为一个 间隔选择 问题。经典的间隔选择问题可以通过动态规划来解决。此类问题的关键在于为每个位置决定“选择”或“不选择”的最佳方案,依次构建出最大化的美味值。
解决思路
动态规划的基本思想是递归地定义状态,将原问题分解为子问题,以避免重复计算。具体步骤如下:
-
定义状态:
-
定义一个函数
dfs(i, pre, ppre)
,表示当前处理第 iii 个糖果时,考虑的状态:- pre\text{pre}pre:表示是否选择了编号为 i−1i-1i−1 的糖果。
- ppre\text{ppre}ppre:表示是否选择了编号为 i−2i-2i−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 的糖果都没有被选择,此时我们有两个选择:
- 跳过当前的第 iii 个糖果,进入到第 i+1i+1i+1 个糖果的决策。
- 选择当前的第 iii 个糖果,此时第 i+1i+1i+1 和第 i+2i+2i+2 个糖果将不能被选择,因此我们递归地进入第 i+1i+1i+1 个糖果的决策,并且标记当前状态为选择了第 iii 个糖果。
-
-
初始状态与边界条件:
- 初始时,我们从第 0 个糖果开始(假设编号从 0 开始),
pre
和ppre
都为 0,表示没有选择之前的糖果。 - 当 iii 达到 nnn 时,表示已经遍历完所有糖果,此时返回 0 表示没有额外的美味值可以增加。
- 初始时,我们从第 0 个糖果开始(假设编号从 0 开始),
-
优化与记忆化:
- 通过
@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)
动态规划转化思路
-
定义状态:用
dp[i]
表示在选择编号为 iii 的糖果时可以获得的最大美味值之和。 -
状态转移方程:
-
对于每一个糖果 iii,我们有两种选择:
- 不选糖果 iii:此时最大美味值和为
dp[i-1]
。 - 选糖果 iii:此时最大美味值和为
a[i] + dp[i-3]
(因为选择了 iii 后,我们不能选择 i−1i-1i−1 和 i−2i-2i−2,所以可以加上dp[i-3]
)。
- 不选糖果 iii:此时最大美味值和为
-
因此状态转移方程为: 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])
-
-
边界条件:
- 当 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])
。
- 当 i=0i = 0i=0 时,
动态规划代码实现
以下是将递归代码转换为迭代写法的动态规划实现:
复制代码
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]
代码说明
-
边界条件处理:我们先处理 n=1n = 1n=1、n=2n = 2n=2 和 n=3n = 3n=3 的情况,以便后续动态规划计算时不越界。
-
初始化 dp 数组:
dp[0]
表示只选择第 0 个糖果的最大美味值,即a[0]
。dp[1]
表示选择前两个糖果中美味值较大的那个,即max(a[0], a[1])
。dp[2]
表示选择不相邻的第 0 和第 2 个糖果的最大美味值,即max(a[0] + a[2], a[1])
。
-
状态转移:从 i=3i = 3i=3 开始,我们根据状态转移方程
dp[i] = max(dp[i-1], a[i] + dp[i-3])
逐步填充dp
数组。 -
最终结果:
dp[-1]
保存了选择糖果的最大美味值之和。