礼盒的最大甜蜜度 图解题解
这道题到底在问什么
- 输入
- price=[13,5,1,8,21,2], k=3
- 输出
- 8
- 输入
- price=[1,3,1], k=2
- 输出
- 2
- 输入
- price=[7,7,7,7], k=2
- 输出
- 0
最优解:一步一步想明白
- 3记牢:甜蜜度 x 越小越容易凑够 k 个,可行性单调,于是在 0 到 max 减 min 上二分 x。检验一个 x:排序后贪心,差不小于 x 就选,选够 k 颗即可达,可达就把左界收到 mid、否则右界降到 mid 减 1。二分的是甜蜜度值,不是下标。
- 4先看清画面。上面这排是原始价格 8,1,5,3,顺序是乱的。我们要从这 4 类里挑 3 类,让盒中两两价差里最小的那个尽量大。右边面板记三件事:甜蜜度的候选范围、当前正在检验的甜蜜度 x、已经选中的糖果数,现在都还没开始。思路不是硬挑,而是二分猜一个甜蜜度 x,再验证它凑不凑得够 3 颗。
- 5第一步先排序,把 8,1,5,3 排成从小到大的 1,3,5,8。为什么必须排序,因为等会儿的贪心是从最小的价格出发、一路往大走,只有有序才能保证「每次尽量早选、给后面留空间」这套贪心成立。排好之后,数轴上这四颗糖果就均匀铺开了,后面所有操作都在这排有序价格上做。
- 6框定答案范围。甜蜜度最小能到 0,比如有两颗价格一样时最小差就是 0;最大不会超过最贵和最便宜之差,也就是 8 减 1 等于 7。所以答案一定落在 0 到 7 这个闭区间里。接下来不逐个试,而是二分:取中点当候选甜蜜度 x,验证「能不能选出 3 颗,让它们两两差都不小于 x」。请记住,右边面板里动的是甜蜜度 x 的范围,数组这排糖果始终不动。
- 7开始第一轮二分。当前范围 0 到 7,取中点,x 等于 0 加 7 加 1 再整除 2 得 4。注意这里取中点特意加了个 1、往上取整,这是「可达就把左界收到 mid」那一支必须的防死循环写法。这一轮就问:能不能选出 3 颗糖果,让它们两两差都不小于 4?下面从最小的糖果开始逐颗贪心地挑。
- 8贪心从最小的 1 开始。这里有个巧劲:把上一次选中的价格 pre 初始设成负的 x,也就是 -4,这样 1 减去 -4 等于 5,一定不小于 4,所以最小的这颗必被选中。选它,已选记 1 颗,把 pre 更新成 1。道理是:最便宜的糖果留着没意义,越早选、越能给后面腾出价格空间。
- 9看价格 3,它减去上一次选中的 1 等于 2。这个差小于门槛 4,离上一颗太近,选了会拉低最小差,跳过它,继续看下一颗。
- 10看价格 5,它减去上一次选中的 1 等于 4。这个差不小于门槛 4,说明它离上一颗够远,选进礼盒,已选变成 2 颗,pre 更新成 5。
- 11看价格 8,它减去上一次选中的 5 等于 3。这个差小于门槛 4,离上一颗太近,选了会拉低最小差,跳过它,继续看下一颗。
- 12一趟贪心扫完,只选到 2 颗,不够 3 颗,说明甜蜜度定到 4 太苛刻,糖果被门槛卡得太狠、凑不齐。那 4 以及更大的都不行,把右界降到 3,范围变成 0 到 3,往更小里找。
- 13第二轮二分。上一轮把范围收到了 [0, 3],取中点得到候选甜蜜度 x 等于 2。同样问一句:能不能选出 3 颗,让两两差都不小于 2?贪心从最小的糖果往上挑。
- 14贪心从最小的 1 开始。这里有个巧劲:把上一次选中的价格 pre 初始设成负的 x,也就是 -2,这样 1 减去 -2 等于 3,一定不小于 2,所以最小的这颗必被选中。选它,已选记 1 颗,把 pre 更新成 1。
- 15看价格 3,它减去上一次选中的 1 等于 2。这个差不小于门槛 2,说明它离上一颗够远,选进礼盒,已选变成 2 颗,pre 更新成 3。
- 16看价格 5,它减去上一次选中的 3 等于 2。这个差不小于门槛 2,说明它离上一颗够远,选进礼盒,已选变成 3 颗,pre 更新成 5。
- 17看价格 8,它减去上一次选中的 5 等于 3。这个差不小于门槛 2,说明它离上一颗够远,选进礼盒,已选变成 4 颗,pre 更新成 8。
- 18一趟贪心扫完,选够了 4 颗,不小于要求的 3 颗,说明甜蜜度做到 2 是办得到的。既然 2 能到,那更大的甜蜜度值得一试,把左界收到 2,范围变成 2 到 3。注意 2 本身可能就是答案,所以左界取 2 而不是 2 加 1,继续往更大里试。
- 19第三轮二分。上一轮把范围收到了 [2, 3],取中点得到候选甜蜜度 x 等于 3。同样问一句:能不能选出 3 颗,让两两差都不小于 3?贪心从最小的糖果往上挑。
- 20贪心从最小的 1 开始。这里有个巧劲:把上一次选中的价格 pre 初始设成负的 x,也就是 -3,这样 1 减去 -3 等于 4,一定不小于 3,所以最小的这颗必被选中。选它,已选记 1 颗,把 pre 更新成 1。
- 21看价格 3,它减去上一次选中的 1 等于 2。这个差小于门槛 3,离上一颗太近,选了会拉低最小差,跳过它,继续看下一颗。
- 22看价格 5,它减去上一次选中的 1 等于 4。这个差不小于门槛 3,说明它离上一颗够远,选进礼盒,已选变成 2 颗,pre 更新成 5。
- 23看价格 8,它减去上一次选中的 5 等于 3。这个差不小于门槛 3,说明它离上一颗够远,选进礼盒,已选变成 3 颗,pre 更新成 8。
- 24一趟贪心扫完,选够了 3 颗,不小于要求的 3 颗,说明甜蜜度做到 3 是办得到的。把左界收到 3,范围变成 3 到 3。左右界撞到一块儿了,二分到这一步就该收束了。
- 25二分收束了。左界和右界都停在 3,说明 3 是能达到的最大甜蜜度:它可达,而比它大的 4 已经验证过办不到。回看这颗最优选择,贪心选出的正是价格 1、5、8 这三类,它们两两差里最小的正好是 3。
- 26收个尾。这道题的巧劲在于把「怎么选 k 类糖果」这个难题,翻译成「甜蜜度能不能达到 x」这个好答的判断题,再靠单调性二分甜蜜度。price 排序成 1,3,5,8、k = 3,我们只检验了 4、2、3 这 3 个候选,就锁定最大甜蜜度是 3。二分把候选个数从线性压成对数级,每次贪心检查线性扫一遍糖果,又快又稳。
⚠️ 容易写错的地方
✗ 错:把二分用在数组下标上,以为是在 price 里找位置
✓ 对:二分的是「甜蜜度 x 这个答案值」,范围 0 到 max 减 min,不是数组下标
这是二分答案最核心的转弯。答案的可行性单调,才把二分从「找元素」升级成「找答案分界点」。误当成下标二分会完全跑偏,而且这里数组必须先排序、贪心才成立
✗ 错:贪心前忘了排序,直接在原数组上挑
✓ 对:一定先把 price 从小到大排好再贪心
贪心靠「从最小出发、每次尽早选」保证给后面留最大空间,只有有序才成立。乱序数组上按同样规则挑会漏掉更优组合,可行性判断就错了
✗ 错:mid 写成 (lo+hi)//2,可达时又写 lo = mid
✓ 对:本支必须 mid = (lo+hi+1)//2 往上取整
在「可达就 lo = mid」这套写法里,若 mid 不往上取整,当 hi 只比 lo 大 1 时 mid 会算成 lo,lo = mid 后范围原地不动,直接死循环。加这个 1 让 mid 偏向右端,才能收敛
✗ 错:门槛判断写成 cur 减 pre 大于 x
✓ 对:应是 cur 减 pre 不小于 x(取等也选)
甜蜜度允许恰好等于 x,差正好是 x 的糖果也该选进来。写成严格大于会把恰好卡在门槛上的糖果漏掉,导致本可达的 x 被误判成不可达,答案偏小
完整代码(Python / C++ / Java)
Python
from __future__ import annotations
from typing import *
from collections import *
from functools import *
from itertools import *
from math import *
from heapq import *
from bisect import *
class Solution:
def maximumTastiness(self, price: List[int], k: int) -> int:
def check(x: int) -> bool:
cnt, pre = 0, -x
for cur in price:
if cur - pre >= x:
pre = cur
cnt += 1
return cnt >= k
price.sort()
l, r = 0, price[-1] - price[0]
while l < r:
mid = (l + r + 1) >> 1
if check(mid):
l = mid
else:
r = mid - 1
return lC++
#include <algorithm>
#include <array>
#include <cctype>
#include <climits>
#include <cmath>
#include <deque>
#include <functional>
#include <iostream>
#include <limits>
#include <map>
#include <numeric>
#include <queue>
#include <set>
#include <sstream>
#include <stack>
#include <string>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
using namespace std;
class Solution {
public:
int maximumTastiness(vector<int>& price, int k) {
sort(price.begin(), price.end());
int l = 0, r = price.back() - price[0];
auto check = [&](int x) -> bool {
int cnt = 0, pre = -x;
for (int& cur : price) {
if (cur - pre >= x) {
pre = cur;
++cnt;
}
}
return cnt >= k;
};
while (l < r) {
int mid = (l + r + 1) >> 1;
if (check(mid)) {
l = mid;
} else {
r = mid - 1;
}
}
return l;
}
};Java
import java.util.*;
class Solution {
public int maximumTastiness(int[] price, int k) {
Arrays.sort(price);
int l = 0, r = price[price.length - 1] - price[0];
while (l < r) {
int mid = (l + r + 1) >> 1;
if (check(price, k, mid)) {
l = mid;
} else {
r = mid - 1;
}
}
return l;
}
private boolean check(int[] price, int k, int x) {
int cnt = 0, pre = -x;
for (int cur : price) {
if (cur - pre >= x) {
pre = cur;
++cnt;
}
}
return cnt >= k;
}
}复杂度
时间
O(n log n + n log C)
n 是糖果类数,C 是价格跨度 max 减 min。先排序是 O(n log n);二分甜蜜度在 0 到 C 上进行,最多 log C 轮,每轮做一次贪心检查、线性扫一遍 n 个价格,是 O(n),合起来 O(n log C)。两部分相加即总时间。演示里 n 等于 4、C 等于 7,排序后只检验了 3 个候选甜蜜度就收敛
空间
O(1)
按峰值算,只计排序之外的额外空间。二分与贪心只用了 lo、hi、mid、cnt、pre 这几个标量,不随规模增长,没有开新数组。若把排序自身的栈开销也算进去,C++ 与 Java 约 O(log n)、Python 的 sort 最坏 O(n);检查逻辑本身是常数额外空间 O(1)
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 礼盒的最大甜蜜度 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么甜蜜度可以拿来二分?单调性体现在哪?+
关键是「甜蜜度能不能达到 x」这个判断关于 x 单调。门槛 x 定得越小,要求两两差都不小于 x 就越松,能塞进礼盒的糖果越多、越容易凑够 k 颗;x 越大越苛刻、越难凑够。于是存在一条分界线,x 小于等于它就可达、超过它就不可达,不会来回横跳。有了单调性,就能像在有序序列里找边界一样二分 x,每次用贪心可行性检查决定往大试还是往小收,把线性枚举压成对数级。
检验一个 x 时,为什么贪心地「能选就选」是对的?+
先排序,再从最小价格往大扫,只要当前价格离上一次选中的够远、差不小于 x 就立刻选。这样做是对的,因为在有序序列里,越早、越靠前地把一颗选进来,给后面剩下的空间就越大,能容纳的后续糖果最多。可以用交换论证:任何一种能选够 k 颗的方案,都能不减少数量地改造成这种贪心方案,所以贪心选出的数量就是这个 x 下的最大可选数,拿它和 k 比即可。
二分为什么用 (lo+hi+1) 右移、可达时又写 lo = mid?+
因为我们要找的是「最大的可达 x」,属于把左界不断往右逼的那类二分。可达时当前 mid 可能就是答案,不能丢,所以写 lo = mid 保留它;不可达才 hi = mid 减 1。但 lo = mid 有个陷阱:当 hi 只比 lo 大 1 时,普通的 (lo+hi) 右移会把 mid 算成 lo,lo = mid 后范围原地不动、死循环。给中点加 1 往上取整,mid 就偏到 hi,收敛得以进行。这是「求最大可行值」二分的固定配套写法。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄声线把 礼盒的最大甜蜜度 一步步讲透(全站 0 节语音精讲,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。