问题描述
小R正在计划一次从地点A到地点B的徒步旅行,总路程需要 N 天。为了在旅途中保持充足的能量,小R每天必须消耗1份食物。幸运的是,小R在路途中每天都会经过一个补给站,可以购买食物进行补充。然而,每个补给站的食物每份的价格可能不同,并且小R最多只能同时携带 K 份食物。
现在,小R希望在保证每天都有食物的前提下,以最小的花费完成这次徒步旅行。你能帮助小R计算出最低的花费是多少吗?
问题分析
这道题的本质是一个动态规划问题,目的是在每天都必须有食物的前提下,通过购买和携带的策略,求解以最小花费完成旅行的方式。问题中涉及以下关键限制:
- 每天必须消耗1份食物。
- 每个补给站的食物价格不同。
- 小R最多只能携带
K份食物。
需要找到最小花费的购买方案,这要求综合考虑:
- 何时购买食物?
- 购买多少食物?
# 代码详解
def solution(n, k, data):
# Edit your code here
dp = [[float('inf')] * (k + 1) for _ in range(n + 1)]
dp[0][0] = 0
for i in range(1, n + 1): # 从第 1 天到第 N 天
for l in range(k): # 当前天结束时剩余食物数 l
for j in range(k): # 前一天剩余的食物数 j
if l - j + 1 >= 0 and l - j + 1 <= k: # 判断购买的食物数量是否合规
dp[i][l] = min(dp[i][l], dp[i - 1][j] + (l - j + 1) * data[i - 1])
# print(dp)
return dp[n][0]
if __name__ == "__main__":
# Add your test cases here
print(solution(5, 2, [1, 2, 3, 3, 2]) == 9)
- 初始化DP表
dp[i][l]表示第i天结束时,剩余l份食物的最小花费。- 二位DP表总共有
n+1行,每一行有k+1列(因为食物剩余量最多为K)。 - 初始化为无穷大(
float('inf')),表示尚未计算或状态不可达。 - 边界条件
dp[0][0] = 0表示第 0 天,没有开始旅程,无需花费,也没有食物剩余。
- 外层循环:
for i in range(1, n + 1)
- 依次计算从第 1 天到第
n天的最小花费。 - 每一天的决策都依赖于前一天的状态。
- 中间循环:
for l in range(k)
- 遍历第
i天结束时剩余的食物数量l,取值范围是[0, K)。 - 目的:找到剩余
l份食物的最小花费。
- 内层循环:
for j in range(k)
- 遍历第
i-1天结束时剩余的食物数量j。 - 目的:计算从前一天
j份食物到当天剩余l份食物所需的花费。
- 状态转移
if l - j + 1 >= 0 and l - j + 1 <= k:
dp[i][l] = min(dp[i][l], dp[i - 1][j] + (l - j + 1) * data[i - 1])
核心判断条件:
l - j + 1 >= 0: 表示需要购买的食物数量为l - j + 1,不能为负数(即不能从未来的状态推导回来)。l - j + 1 <= k: 表示购买的食物数量不能超过携带上限K。
转移方程:
dp[i][l] = min(dp[i][l], dp[i - 1][j] + (l - j + 1) * data[i - 1]):- 从前一天的状态
dp[i-1][j]转移到今天的状态dp[i][l]。 - 转移的花费为
- 前一天的花费
dp[i-1][j]。 - 购买
l - j + 1份食物的额外花费:(l - j + 1) * data[i-1](第i-1天的补给站食物单价为data[i-1])。
- 前一天的花费
- 从前一天的状态
转移结果:
- 动态更新
dp[i][l],保证对于每一天结束时剩余l份食物,始终保持最小花费。
- 输出结果
最终输出 dp[n][0],表示第 n 天结束时,无剩余食物(0 份)的最小花费。
代码逻辑验证
输入示例:
n = 3 # 总共3天
k = 2 # 最多携带2份食物
data = [2, 3, 1] # 每天的食物单价
初始状态:
dp = [
[0, inf, inf], # 第0天:无花费
[inf, inf, inf], # 第1天
[inf, inf, inf], # 第2天
[inf, inf, inf] # 第3天
]
第1天:
剩余 l = 0,购买1份(消耗当天需要的):
dp[1][0] = dp[0][0] + 1 * 2 = 2
剩余 l = 1,购买2份(1份当天需要,1份备用):
dp[1][1] = dp[0][0] + 2 * 2 = 4
第2天:
剩余 l = 0:
dp[2][0] = min(dp[1][0] + 1 * 3, dp[1][1] + 0 * 3) = 5
剩余 l = 1:
dp[2][1] = min(dp[1][0] + 2 * 3, dp[1][1] + 1 * 3) = 7
第3天:
剩余 l = 0:
dp[3][0] = min(dp[2][0] + 1 * 1, dp[2][1] + 0 * 1) = 6
最终 dp 表:
dp = [
[0, inf, inf],
[2, 4, inf],
[5, 7, inf],
[6, inf, inf]
]
输出结果:6
总结
1. 动态规划的核心思想
- 递归子问题:旅行第
i天的决策依赖于前一天的状态以及当前的购买策略。 - 最优子结构:如果前
i-1天的决策已经是最优,则第i天在其基础上作出的决策也是最优。 - 重叠子问题:第
i天的计算需要多次重复利用前一天的状态,因此可以用动态规划存储中间结果。
- 二维 DP 的状态设计
-
将状态设计为二维数组
dp[i][l],通过两个维度描述问题的不同变量:- 天数(时间维度)。
- 剩余食物数量(背包维度)。
- 状态转移与条件约束
-
状态转移时,通过明确的边界条件控制合理性:
- 剩余食物不能超过最大携带量。
- 每天至少消耗1份食物。
-
用条件约束减少非法状态的计算,提升算法效率。
- 最小值优化问题的思想
- 动态规划常用于求解最值问题(最小值或最大值)。这里通过多次迭代更新
dp表,最终得到全局最优解。