leetcode 1774. 最接近目标价格的甜点成本
1774. 最接近目标价格的甜点成本
你打算做甜点,现在需要购买配料。目前共有 n
种冰激凌基料和 m
种配料可供选购。而制作甜点需要遵循以下几条规则:
- 必须选择 一种 冰激凌基料。
- 可以添加 一种或多种 配料,也可以不添加任何配料。
- 每种类型的配料 最多两份 。
给你以下三个输入:
baseCosts
,一个长度为n
的整数数组,其中每个baseCosts[i]
表示第i
种冰激凌基料的价格。toppingCosts
,一个长度为m
的整数数组,其中每个toppingCosts[i]
表示 一份 第i
种冰激凌配料的价格。target
,一个整数,表示你制作甜点的目标价格。
你希望自己做的甜点总成本尽可能接近目标价格 target
。
返回最接近 target
的甜点成本。如果有多种方案,返回 成本相对较低 的一种。
示例 1:
输入:baseCosts = [1,7], toppingCosts = [3,4], target = 10 输出:10 解释:考虑下面的方案组合(所有下标均从 0 开始): - 选择 1 号基料:成本 7 - 选择 1 份 0 号配料:成本 1 x 3 = 3 - 选择 0 份 1 号配料:成本 0 x 4 = 0 总成本:7 + 3 + 0 = 10 。
示例 2:
输入:baseCosts = [2,3], toppingCosts = [4,5,100], target = 18 输出:17 解释:考虑下面的方案组合(所有下标均从 0 开始): - 选择 1 号基料:成本 3 - 选择 1 份 0 号配料:成本 1 x 4 = 4 - 选择 2 份 1 号配料:成本 2 x 5 = 10 - 选择 0 份 2 号配料:成本 0 x 100 = 0 总成本:3 + 4 + 10 + 0 = 17 。不存在总成本为 18 的甜点制作方案。
示例 3:
输入:baseCosts = [3,10], toppingCosts = [2,5], target = 9 输出:8 解释:可以制作总成本为 8 和 10 的甜点。返回 8 ,因为这是成本更低的方案。
示例 4:
输入:baseCosts = [10], toppingCosts = [1], target = 1 输出:10 解释:注意,你可以选择不添加任何配料,但你必须选择一种基料。
提示:
n == baseCosts.length
m == toppingCosts.length
1 <= n, m <= 10
1 <= baseCosts[i], toppingCosts[i] <= 104
1 <= target <= 104
解题思路:
#必须选一种基料,所以选代价最小的基料
# 辅料可以不选、选一个,选两个,可用dp或者回溯枚举
方法一:回溯
思路与算法
首先题目给出长度分别为 nn 的冰淇淋基料数组 \textit{baseCosts}baseCosts 和长度为 mm 的配料数组 \textit{toppingCosts}toppingCosts,其中 \textit{baseCosts}[i]baseCosts[i] 表示第 ii 种冰淇淋基料的价格,\textit{toppingCosts}[j]toppingCosts[j] 表示一份第 jj 种冰淇淋配料的价格,以及一个整数 \textit{target}target 表示我们需要制作甜点的目标价格。现在在制作甜品上我们需要遵守以下三条规则:
必须选择一种冰淇淋基料;
可以添加一种或多种配料,也可以不添加任何配料;
每种配料最多两份。
我们希望做的甜点总成本尽可能接近目标价格 \textit{target}target,那么我们现在按照规则对于每一种冰淇淋基料用回溯的方式来针对它进行甜品制作。又因为每一种配料都是正整数,所以在回溯的过程中总开销只能只增不减,当回溯过程中当前开销大于目标价格 \textit{target}target 后,继续往下搜索只能使开销与 \textit{target}target 的差值更大,所以如果此时差值已经大于等于我们已有最优方案的差值,我们可以停止继续往下搜索,及时回溯。
class Solution: def closestCost(self, baseCosts: List[int], toppingCosts: List[int], target: int) -> int: ans = min(baseCosts) def dfs(p: int, cur_cost: int) -> None: nonlocal ans if abs(ans - target) < cur_cost - target: return if abs(ans - target) >= abs(cur_cost - target): if abs(ans - target) > abs(cur_cost - target): ans = cur_cost else: ans = min(ans, cur_cost) if p == len(toppingCosts): return dfs(p + 1, cur_cost + toppingCosts[p] * 2) dfs(p + 1, cur_cost + toppingCosts[p]) dfs(p + 1, cur_cost) for c in baseCosts: dfs(0, c) return ans
方法二:动态规划
思路与算法
我们可以将问题转化为对于某一个开销是否存在甜品制作方案问题,然后我们选择与目标价格最接近的合法甜品制作方案即可,那么问题就转化为了「01 背包」问题(关于「01 背包」的概念可见 百度百科)。这样我们就可以把问题求解从指数级别降到多项式级别了。对于「01 背包」的求解我们可以用「动态规划」来解决。
设最小的基料开销为 xx。若 x \ge \textit{target}x≥target,则无论我们是否添加配料都不会使甜品制作的开销与目标价格 \textit{target}target 的距离缩小,所以此时直接返回此最小的基料开销即可。当最小的基料开销小于 \textit{target}target 时,我们可以对超过 \textit{target}target 的制作开销方案只保存其最小的一份即可,并可以初始化为 2 \times \textit{target} - x2×target−x,因为大于该开销的方案与目标价格 \textit{target}target 的距离一定大于仅选最小基料的情况,所以一定不会是最优解。将背包的容量 \textit{MAXC}MAXC 设置为 \textit{target}target。然后我们按「01 背包」的方法来依次枚举配料来进行放置。
我们设 \textit{can}[i]can[i] 表示对于甜品制作开销为 ii 是否存在合法方案,如果存在则其等于 \text{true}true,否则为 \text{false}false,初始为 \text{false}false。因为单独选择一种基料的情况是合法的,所以我们对 \textit{can}can 进行初始化:
\textit{can}[x] = \text{true}, \forall x \in \textit{baseCosts} \And x < \textit{MAXC}
can[x]=true,∀x∈baseCosts&x<MAXC
然后我们按「01 背包」的方法来依次枚举配料来进行放置,因为每种配料我们最多只能选两份,所以我们可以直接将每种配料变为两个,然后对于两个配料都进行放置即可。因为任意一个合法方案加上一份配料一定也为合法制作方案。所以当要放置的配料开销为 yy 时,对于开销为 c, c > yc,c>y 的转移方程为:
\textit{can}[c] = \textit{can}[c - y] ~|~ \textit{can}[c], c > y
can[c]=can[c−y] ∣ can[c],c>y
因为每一个状态的求解只和前面的状态有关,所以我们可以从后往前来更新每一个状态。然后当配料全部放置后,我们可以从目标价格 \textit{target}target 往左搜索找到最接近 \textit{target}target 的合法方案并与大于 \textit{target}target 的方案做比较返回与 \textit{target}target 更接近的方案即可。
class Solution: def closestCost(self, baseCosts: List[int], toppingCosts: List[int], target: int) -> int: x = min(baseCosts) if x > target: return x can = [False] * (target + 1) ans = 2 * target - x for c in baseCosts: if c <= target: can[c] = True else: ans = min(ans, c). #必须选一种基料,所以选代价最小的基料
# 辅料可以不选、选一个,选两个,可用dp或者回溯枚举 for c in toppingCosts: for count in range(2): for i in range(target, 0, -1): if can[i] and i + c > target: ans = min(ans, i + c) if i - c > 0 and not can[i]: can[i] = can[i - c] for i in range(ans - target + 1): if can[target - i]: return target - i return ans