本系列将分享十一月在marscode上遇到的比较有意思的题目供同学们交流分享。
本题将用到一个效率很高的查找算法,想必你已经猜到了,没错就是二分查找。
问题描述
小明的蛋糕工厂每天可以生产的蛋糕数量是由工厂中的机器和工人的数量决定的,即 ( m \times w )。现在他收到了一个大订单,需要尽快生产出 ( n ) 个蛋糕。为了提升生产速度,小明可以使用每天生产的蛋糕去购买额外的机器或工人,每台机器或每个工人的成本是 ( p ) 个蛋糕。
例如,如果工厂起始时有 1 台机器和 2 个工人,每次扩大产能的成本是 1 个蛋糕,为了生产 60 个蛋糕,小明可以这样操作:
- 第一天:生产 2 个蛋糕,买入 2 台机器(总机器数变为 3)。
- 第二天:生产 6 个蛋糕,买入 3 台机器,3 个工人(机器数 6,工人数 5)。
- 第三天:生产 30 个蛋糕。
- 第四天:再生产 30 个蛋糕,完成订单。
你的任务是帮助小明计算最快多少天能完成订单。
样例1:
输入:
m = 3, w = 1, p = 2, n = 12
输出:3
样例2:
输入:
m = 10, w = 5, p = 30, n = 500
输出:8
样例3:
输入:
m = 3, w = 5, p = 30, n = 320
输出:14
首先进行思路分析:
1. 整体思路
- 我们的目标是找到最快完成订单所需的天数。由于直接通过逐步模拟每天的生产和扩张来找到这个天数可能效率不高(尤其当订单规模大、初始产能和扩张条件复杂时),所以考虑使用二分查找来优化这个搜索过程。
- 二分查找的关键在于确定一个合理的查找范围,并根据每次猜测值的判断结果来不断缩小这个范围,直到找到满足条件的最优解(在这里就是最快完成订单的天数)。
2. 确定查找范围
- 下限(左边界) :最少天数肯定要大于等于按照初始产能一直生产到完成订单所需的天数。通过简单计算
n / (m * w)(这里是整数除法),可以得到在不考虑扩张产能的情况下,仅依靠初始的机器数量m和工人数量w生产完n个蛋糕所需的最少天数,这个值可作为二分查找范围的下限。 - 上限(右边界) :从理论上来说,假设一种极端情况,即每天只生产一个蛋糕(因为可以用生产的蛋糕去买机器和工人来扩大产能,所以最慢的情况可近似看作每天只增加一点点产能,类似每天只生产一个蛋糕),那么最多需要
n天来完成订单,所以n可作为二分查找范围的上限。
3. 判断条件设定
- 在二分查找过程中,对于每次猜测的天数
mid,我们需要模拟在这个天数内按照给定的生产和扩张规则进行操作,看最终是否能够完成订单。 - 如果在
mid天内能够完成订单,这意味着实际完成订单所需的天数可能小于等于mid,所以我们要将查找范围缩小到左半区间继续查找(因为我们要找的是最少天数,也就是最接近下限的那个能完成订单的天数)。 - 相反,如果在
mid天内不能完成订单,那就说明实际完成订单所需的天数大于mid,此时需要将查找范围缩小到右半区间继续查找。
代码最关键部分——核心算法
定义一个fastest_day 函数实现二分:
int fastest_days(int m, int w, int p, int n) {
int left = n / (m * w);
int right = n;
while (left < right) {
int mid = left + (right - left) / 2;
if (canCompleteOrder(m, w, p, n, mid)) {
right = mid;
} else {
left = mid + 1;
}
}
return left;
}
-
具体的二分查找过程:
-
此函数接受初始的机器数量
m、工人数量w、每个机器或工人的成本p以及订单需要生产的蛋糕总数n作为参数,用于通过二分查找找到最快完成订单的天数。 -
首先确定二分查找的左右边界,左边界
left初始化为n / (m * w),右边界right初始化为n。 -
进入一个循环,只要
left小于right,就继续循环执行以下操作:- 计算中间天数
mid,采用left + (right - left) / 2的方式计算(这样可以避免整数溢出问题)。 - 调用
canCompleteOrder函数,传入当前的参数以及猜测的天数mid,判断在mid天内是否能够完成订单。 - 根据
canCompleteOrder函数的返回结果来调整查找范围:如果能完成订单,将右边界right调整为mid;如果不能完成订单,将左边界left调整为mid + 1。
- 计算中间天数
-
-
返回结果:
- 当循环结束时(即
left等于right),此时的left值就是最快完成订单所需的天数,将其返回作为最终结果。
- 当循环结束时(即
最终代码
#include <stdio.h> #include <stdlib.h>
int canCompleteOrder(int m, int w, int p, int n, int days) {
int current_m = m;
int current_w = w;
int total_cakes_produced = 0;
for (int i = 0; i < days; i++)
{
int cakes_produced_today = current_m * current_w;
total_cakes_produced += cakes_produced_today;
int new_assets = cakes_produced_today / p;
if (current_m <= current_w) { current_m += new_assets; }
else { current_w += new_assets; }
}
return total_cakes_produced >= n;
}
int fastest_day(int m, int w, int p, int n)
{ int left = n / (m * w);
int right = n;
while (left < right)
{
int mid = left + (right - left) / 2;
if (canCompleteOrder(m, w, p, n, mid))
{ right = mid; }
else { left = mid + 1; }
} return left;
}
int main() { int m = 1;
int w = 2;
int p = 1;
int n = 60;
int result = fastest_day(m, w, p, n);
printf("最快完成订单所需天数: %d\n", result);
return 0;
}
刷题小结
我们学到了一个高效的查找算法二分 高效就高效在:
时间复杂度
该算法的时间复杂度为O(log n)。随着数据量n的增加,查找次数增长缓慢。
逻辑
核心逻辑是通过比较目标元素与中间元素的大小,不断将查找区间减半。这种基于中间元素划分区间的方式直观易懂,易于编程实现。
利用数据有序性
特别适合有序数据。在实际应用中,像排好序的数据库索引、有序的文件列表等场景,二分查找能发挥优势,快速定位目标元素,提高查找效率。
同学们,从一次又一次的写题中应该能感受到算法是编程“硬核”力量。它既能优化程序又能攻克难题,贯穿科技应用。多种多样的算法学起来虽有挑战,但突破后可夯实根基、提升素养。把握当下,开启求知,拥抱无限可能!
共勉!