二分查找专题(十三):“答案二分”的“三连击”——「制作m束花所需的最少天数」
哈喽各位,我是前端小L。
欢迎来到我们的二分查找专题第十三篇!“答案二分”的威力,在于它能“四两拨千斤”,将一个复杂的最优化问题,转化为一个简单的“Yes/No”判定问题。
今天,我们将用这个“大杀器”,去解决一个关于“等待”和“制作”的优美问题。我们要找的,不再是“最小速度”,也不是“最低运力”,而是“最少天数”。
力扣 1482. 制作 m 束花所需的最少天数
https://leetcode.cn/problems/minimum-number-of-days-to-make-m-bouquets/

题目分析:
-
输入:花朵的开花日期数组
bloomDay,目标花束m,每束花需要k朵相邻的花。 -
目标:找到一个最少的天数
d,使得在第d天时,我们可以从花园里采到m束花。 -
规则:只有在
bloomDay[i] <= d时,第i朵花才能被采摘。
核心洞察:d (天数) 的单调性 这个“最少天数 d”,就是我们要二分查找的“答案”。
-
让我们来验证一下“单调性”。
-
假设我们有一个
check(days)函数,它能判断在days天后,能否制作m束花。 -
如果
check(10) == true(第10天,能做够),那么在第11天(花开得更多了),也一定能做够 (true)。 -
如果
check(10) == false(第10天,做不够),那么在第9天(花开得更少),一定也做不够 (false)。
这又是在“答案范围”(即天数范围)上,形成了一个我们无比熟悉的隐式数组: [F, F, F, ..., F, T, T, T, ...] 我们的目标,就是找到第一个 T!这又双叒是一个**lower_bound** 问题!
“答案二分”的建模
1. check(days) 函数的实现 (O(n)) 这是“答案二分”的“判定”核心。给定一个 days,能否制作 m 束花?
-
我们可以用贪心策略来验证:
-
初始化
bouquets_made = 0(已做花束),flowers_in_a_row = 0(当前连续开花的数量)。 -
遍历
bloomDay数组,索引为i:-
if (bloomDay[i] <= days):-
太好了,这朵花开了!
-
flowers_in_a_row++。 -
检查能否做成一束花:
if (flowers_in_a_row == k)-
成功!
bouquets_made++。 -
重置连续计数:
flowers_in_a_row = 0。
-
-
-
else(bloomDay[i] > days):-
这朵花没开。
-
连续性被打破!
-
flowers_in_a_row = 0。
-
-
-
遍历结束后,返回
bouquets_made >= m。
2. 搜索范围 [left, right) 的确定
-
前置判断:如果需要的总花朵数
m * k超过了数组长度n,那么永远不可能做成,直接返回-1。(这是一个高质量的“剪枝”) -
left(最少可能天数):*min_element(bloomDay.begin(), bloomDay.end())。 -
right(最大可能天数):*max_element(bloomDay.begin(), bloomDay.end())。 -
区间定义:
[left, right)->[min_day, max_day + 1)。-
left = min_day。 -
right = max_day + 1。(+1是为了让max_day这个可能的答案,包含在我们的左闭右开区间内)。
-
3. 套用“万能模板”
-
left = min_day,right = max_day + 1 -
while (left < right) -
mid = ... -
if (check(mid))(即mid天可行,T):-
mid可能是答案,但我们想找“最少”的,所以试试更小的。 -
答案在左侧
[left, mid) -
right = mid
-
-
else(!check(mid)) (即mid天不行,F):-
mid肯定不是答案,答案在右侧。 -
left = mid + 1
-
-
最终答案:
left。(如果left超过了max_day,说明无解,但我们的check函数会保证,如果check(max_day)都是false,left会停在max_day + 1。我们可以在最后判断一下,或者在check的true分支里记录ans)
代码实现
#include <vector>
#include <numeric>
#include <algorithm> // for min_element, max_elementusing namespace std;class Solution {
private:// 判定函数:在 'days' 天内,能否制作 m 束花,每束 k 朵bool check(vector<int>& bloomDay, int m, int k, int days) {int bouquets_made = 0;int flowers_in_a_row = 0;for (int day : bloomDay) {if (day <= days) { // 花开了flowers_in_a_row++;if (flowers_in_a_row == k) {bouquets_made++;flowers_in_a_row = 0;}} else { // 花没开,打断了连续性flowers_in_a_row = 0;}}return bouquets_made >= m;}public:int minDays(vector<int>& bloomDay, int m, int k) {int n = bloomDay.size();// 剪枝:花的总数都不够if ((long long)m * k > n) {return -1;}// 1. 确定答案范围 [left, right)int left = *min_element(bloomDay.begin(), bloomDay.end());int right = *max_element(bloomDay.begin(), bloomDay.end()) + 1;int ans = -1; // 用于记录可行的答案// 2. 循环while (left < right) {// 3. midint mid = left + (right - left) / 2;// 4. 指针移动if (check(bloomDay, m, k, mid)) {// mid 天可行 (T),可能是答案,尝试更小的ans = mid; // 记录下这个可行的答案right = mid;} else {// mid 天不行 (F),必须等更久left = mid + 1;}}// 循环结束,left (或 right) 指向第一个 T// 或者,我们返回最后一次 check 为 true 时的 ans// 如果 check 从未为 true(比如 m*k > n),ans 会保持 -1// 但我们已经在开头剪枝了。// 如果 check(max_day) 都是 false, ans 保持 -1, left 会变成 max_day+1。// 但根据题意,如果可达,一定在 [min_day, max_day] 内。// 所以,循环结束时,left 就是最小天数。// (如果 check(max_day) 为 false, left 会=max_day+1, right 会=max_day+1)// (如果 check(min_day) 为 true, right 会=min_day, left 会=min_day)// 我们的模板,最终 left 就是答案// 思考:如果无解怎么办?比如 `m=1, k=1`, `bloomDay=[10]`. `left=10, right=11`. `mid=10`. `check(10)`=true. `ans=10, right=10`. `[10, 10)` 结束。返回 `left=10`。// 如果 `m=2, k=1`, `bloomDay=[10]`. `m*k > n` -> -1.// 如果 `m=1, k=2`, `bloomDay=[10]`. `m*k > n` -> -1.// 所以,只要过了剪枝,就一定有解return left;}
};
深度复杂度分析
-
check(days)函数:-
遍历
bloomDay数组一次,O(n),其中n是bloomDay.size()。
-
-
二分查找:
-
搜索范围是
[min_day, max_day]。设M = max(bloomDay)。 -
循环次数为 O(log M)。
-
-
总时间复杂度:
-
O(n * log M)。
-
-
空间复杂度 O(1):
-
我们只使用了常数个额外变量。
-
总结
今天这道题,是“答案二分”模型的第三次精彩演绎。 我们已经形成了一个肌肉记忆般的“三部曲”:
-
识别模型:“最小化...天数”、“最小化...速度”、“最小化...运力” -> 答案二分。
-
分析单调性:
check(answer)必须满足[F...T]结构。 -
构建
check函数:用 O(n) 的贪心(或其他方法)来验证一个“猜测”的答案是否可行。
从“珂珂”、“运包裹”到“做花束”,问题的“外壳”在变,但“答案二分 + O(n)贪心验证”的算法内核,坚如磐石。
下期见!
