1774. 最接近目标价格的甜点成本
你打算做甜点,现在需要购买配料。目前共有 n
种冰激凌基料和 m
种配料可供选购。而制作甜点需要遵循以下几条规则:
- 必须选择 一种 冰激凌基料。
- 可以添加 一种或多种 配料,也可以不添加任何配料。
- 每种类型的配料 最多两份 。
给你以下三个输入:
-
baseCosts
,一个长度为 n
的整数数组,其中每个 baseCosts[i]
表示第 i
种冰激凌基料的价格。 -
toppingCosts
,一个长度为 m
的整数数组,其中每个 toppingCosts[i]
表示 一份 第 i
种冰激凌配料的价格。 -
target
,一个整数,表示你制作甜点的目标价格。
你希望自己做的甜点总成本尽可能接近目标价格 target
。
返回最接近 target
的甜点成本。如果有多种方案,返回 成本相对较低 的一种。
提示
-
n == baseCosts.length
-
m == toppingCosts.length
-
1 <= n, m <= 10
-
1 <= baseCosts[i], toppingCosts[i] <= 10^4
-
1 <= target <= 10^4
示例
思路
回放
由于自己还比较菜,所以当时的思路一通乱飞。但是记录下自己思考的过程我觉得挺好
还原一下我当时的脑子:
先从基料中
n
选1,然后每种配料可不选,可选1份,可选2份。又是组合类型的题目。那就先看看能不能暴力做咯。基料n
种,配料m
种。n
和m
最大为10。基料n
选1,先乘个n
,每种配料有3种选择:不选,选1份,选2份。那么总共的方案数就是,我超!已经到以上了,那暴力肯定超时了!不行,得换思路。
(真是个猪脑子,怎么推出来 的?)
我仔细回想了下当时的情景,
每种配料有3种选择,那
m
种配料一共就能形成3m
种选项吧,最大也就是30。对于每个选项,我都有选或不选两种决策吧。那组合起来总共的方案数就是
真是个猪脑子 × 2
其实暴力的时间复杂度应该只能达到 。是这样 → 一共最多10种配料,每种配料我有3个选项,根据乘法原理,10种配料能组合出的方案数就是 ,大概在 ,算上基料的n
,一共也就 ,这妥妥的不超时啊。
暴力做法这里先按下不表,继续记录当时的思考过程
在添加配料的过程中,成本是不断变大的,也就是成本较大的方案一定是由成本较小的方案,添加某种配料转移过来的。诶!有点动态规划那意思了。但怎么来表示状态呢?前面分析过了,配料的选择方案一共有 ,这如果直接作为状态表示肯定不行,已经到 个状态了,每个状态的计算就算是 也会超时。诶!一共有 种情况,恰好在
int
的范围内,是不是在暗示我用状态压缩呢?用一个int
的二进制表示来表达每种配料的选择情况,每个二进制位是0或1,0表示选,1表示不选,30个二进制位就足够表达用状态压缩的话,状态表示好像没问题了,但怎么考虑状态的转移呢? ?&^*9#$
状态压缩好像走不下去了。
(大脑运转中…)
诶!如果将基料和配料的成本看成体积,每次选基料或配料时,就是往背包里扔进一个物品。是不是有点像背包问题呢?好像是的哦!
如果用
dp[i][j]
来表示状态,那它应该表示的是,在前i
种配料中做选择,成本总和恰好为j
的方案,具体表示啥呢?那就表示这种方案的总成本吧?那状态数组第二个维度要开多大呢?算下能形成的最大总成本就行了,那就是选成本最大的基料,并且每种配料全都加2份。那最后的答案就是,遍历下所有能构成的总成本,取其中距离
target
最近的就行了!由于每种配料可以不选,选1份,选2份。那这其实就是一个分组背包。
一种配料就是一个分组,这个分组里有3个物品,分别是:不选该配料,选1份该配料,选2份该配料。
基料是特殊的一个分组,这个分组里的物品数量就是基料的种类数,需要从中选择一种基料。
在每个分组里,我们只能从该分组中选1个物品。
分析到这里就搞定啦!下面直接套用分组背包的模板就好啦!
在计算
dp[i][j]
时,我们考虑第i
组的情况,我们枚举第i
组的每个元素的情况,依次计算状态转移即可。
dp[i][j] = min(dp[i][j], dp[i - 1][j - x] + x)
(x
是第i
组当前被选中的元素的成本)来算下时间复杂度,能形成的最大成本,算了下是 ,总共的状态数是 级别,每个状态的转移,需要枚举该分组的元素个数,一共最多有11个分组(1个基料组+10个配料组),全部分组的元素个数是 10 + 3 × 10 = 40,平均一个组的元素个数是 40 / 11,大概是4个,那么总的时间复杂度大概在
于是噼里啪啦敲键盘…得到了如下这份代码
然后提交,发现竟然通过了!虽然耗时 1200ms
,击败了 5%
的用户
上面的时间复杂度的分析是正确的,但已经快到
总结下自己的思考过程:
→ 暴力?→ 错误的高估了时间复杂度,舍弃 → 动态规划 ? → 状压? → 状压想不通 → 背包问题?→ 分组背包?→ 好像可以
正解
暴搜
其实这道题的数据范围,暴力是可以求解的,基料最多为10,配料最多为10,每种配料有3种选项,暴力的时间复杂度为 ,大概在 。
提交发现跑的很快
动态规划
再看一种动态规划的做法。假设有某种方案,能够凑出成本j
,那么此时,对于某种还未添加过的配料,我们可以选择不添加,添加1份,添加2份,假设该配料成本为x
,那么我们能够凑出成本j + x
,j + 2x
。
我们设dp[i][j]
表示,只考虑前i
个配料,是否能够凑出成本j
,若能凑出,则dp[i][j] = true
,否则dp[i][j] = false
。
状态转移的逻辑为:只考虑前i - 1
个配料时,如果有某个j
满足dp[i - 1][j] = true
,那么可以更新dp[i][j] = dp[i][j + x] = dp[i][j + 2x] = true
。
最后只需要遍历一下j
,找到最接近target
的成本即可。
我们需要看一下第二维的j
,需要开多大。主要是需要考虑超过target
的情况。
由于基料必选,配料可选,我们能组合出的最小的成本,就是选择成本最小的基料,并且配料一个也不选。此时的方案就是成本最低的,设为min
。
假设min < target
,我们再看一下超过target
的,距离同等的成本,容易算得是2 * target - min = upper
。当超过upper
后,再增加成本,其与target
的距离一定比min
与target
的距离更远,是一定不会作为答案输出的。并且如果距离最近的方案取到了min
和upper
,一定是选择成本更小的min
。
所以,第二维的j
只需要枚举到upper - 1
即可。
由于每一行的全部状态只依赖于上一行,所以可以用滚动数组思想,把第一维去掉(去掉行,只保留列),变成dp[j]
,其表示,(状态转移过程中)某一行的所有列的状态。但是需要从右往左更新。
如果dp[i - 1][j] = true
,设第i
种配料的代价为x
,则可以更新 dp[i][j] = dp[i][j + x] = dp[i][j + 2x] = true
。根据转移方程,某一个状态,依赖于其上一行,更左侧的列的状态(依赖于左上方的状态)。从右往左更新,这样在走到某个位置时,计算所需要用到的更左侧的状态的值,仍然是更新前的值(上一行的状态),这样才能保证状态转移的正确性。