难度中等55
你打算做甜点,现在需要购买配料。目前共有 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.lengthm == toppingCosts.length1 <= n, m <= 101 <= baseCosts[i], toppingCosts[i] <= 1041 <= target <= 104DFS得到配料所有可能的成本,然后对每一种基料找到最接近target的。
class Solution {
int best = (int)1e9;
int target;
public int closestCost(int[] baseCosts, int[] toppingCosts, int target) {
this.target = target;
for(int i = 0; i < baseCosts.length; i++){
dfs(toppingCosts, 0, baseCosts[i]);
}
return best;
}
//暴搜,数据量很小,枚举每个选择就可以
public void dfs(int[] arr, int idx, int cost){
int sign = Math.abs(best-target) - Math.abs(cost-target);
// sign > 0 : 说明当前cost比best离target的差值小
//(sign == 0 && cost < best) : 成本相同,且成本比best低
if(sign > 0 || (sign == 0 && cost < best)){
best = cost;
}
//剪支以下,如果当前的成本已经大于cost,没有必要再往后就行选择
//或者已经遍历完,结束
if(cost >= target || idx == arr.length){
return;
}
//每个配料可以选0,1,2份
for(int k = 0; k < 3; k++){
dfs(arr, idx+1, cost + arr[idx]*k);
}
}
}
动态规划,0-1背包问题来做。看成本选得到,选不到的问题。冰淇凌选且仅选一个,所以每个冰淇凌的成本都能选到。然后配料可以选0-2份,遍历配料,看在这基础上有哪些成本可以取到。然后看能取到的成本中,哪个最接近target。时间复杂度O(n+m⋅20000)。
public class leetcode5690 {
// 动态规划,0-1背包问题
public int closestCost(int[] baseCosts, int[] toppingCosts, int target) {
int n = baseCosts.length;
int m = toppingCosts.length;
// target最多10000,冰淇凌和配料也最多一万,答案不会超过20000
boolean[] dp = new boolean[20001];
// 每个冰淇凌只能选一个
for(int i = 0;i < n;i++){
dp[baseCosts[i]] = true;
}
// 配料可以选0-2份,扩展一倍
int[] topping = Arrays.copyOf(toppingCosts, m * 2);
for(int i = m;i < 2 * m;i++){
topping[i] = topping[i-m];
}
// 枚举物品
for(int i = 0;i < 2 * m;i++){
int cur = topping[i];
// 如果前一个成本能选到,加了这个配料的价格后,也能选到
for(int j = 20000;j > cur;j--){
dp[j] = dp[j] || dp[j-cur];
}
}
int ans = 0;
int dif = 30000;
// 看可以选到的成本,哪个最接近target
for(int i = 1;i < 20001;i++){
int cur = Math.abs(i-target);
if(dp[i] && cur < dif){
dif = cur;
ans = i;
}
}
return ans;
}
}
考虑到baseCosts、toppingCosts的长度最多都为10,每一种辅料都有加0、1、2份的选择,因此可以考虑三进制状态压缩求解。
类似二进制的状态压缩。
class Solution {
public int closestCost(int[] baseCosts, int[] toppingCosts, int target) {
Arrays.sort(baseCosts);
Arrays.sort(toppingCosts);
int nearestCost=0;//记录最接近的成本
int minGap = Integer.MAX_VALUE;//记录目前最接近的成本和目标成本target的差
for(int i=0;i<baseCosts.length;i++){//每一种基料
for(int j=0;j<Math.pow(3,toppingCosts.length);j++){//三进制位串
int cost = baseCosts[i];
int curJ = j;//现在的位串
int index = 0;
while(curJ!=0){//取位串的每一位
int curBit = curJ % 3;//位串的当前一位
curJ /= 3;
cost += toppingCosts[index]*curBit;
index++;
}
if(Math.abs(cost-target)==0){//若现在的成本恰好是target,直接返回
return target;
}else if(Math.abs(cost-target)<minGap){//若目前的成本更接近target,更新nearestCost和minGap;
minGap = Math.abs(cost-target);
nearestCost = cost;
}else if(Math.abs(cost-target)==minGap){//若目前的成本和已经找到的最接近成本与目标成本的差距相等,取成本较小者(题目要求)
if(cost<nearestCost){
nearestCost = cost;
}
}
}
}
return nearestCost;
}
}
补充
假如一个数的三进制为2 1 2 0 0,则
int sum = 0;
int idx = i;
int cur = 0;
while(idx!=0){
sum+=toppingCosts[cur]*(idx%3);
idx/=3;
cur++;
}
则该代码块执行的功能就是 toppingCosts的 [0]·0 + [1]·[0] + [2]·2 + [3]·1 + [4]·2
枚举完辅料的所有可能后开始枚举主料,依次比较即可。