最佳蔬菜配送方案
背景介绍
疫情期间,需要组织志愿者为一批社区的家庭配送爱心蔬菜。每个社区的家庭数量不同,而每个志愿者的时间和精力是有限的。为了尽快完成任务,需要你设计一个最高效的分配方案。
核心概念
-
社区 (
communities):- 这是一个按顺序排列的社区列表。
communities[i]表示第i个社区的家庭数量。
-
志愿者 (Volunteer):
- 共有
num名志愿者可以同时开始工作(并行配送)。
- 共有
-
配送任务 (Delivery Task):
- 每个志愿者负责配送连续的若干个社区。例如,可以负责
[社区0, 社区1, 社区2],但不能跳着负责[社区0, 社区2]。 - 每个社区只能由一名志愿者负责。
- 每个志愿者负责配送连续的若干个社区。例如,可以负责
-
耗时计算 (Time Calculation):
- 为每个家庭配送耗时均为 1 小时。
- 单个志愿者的耗时 = 他所负责的所有社区的家庭数量之和。
- 总任务耗时 = 所有志愿者中,那个耗时最长的人所花的时间。因为所有人是并行工作的,最终的完成时间取决于最慢的那个人。
目标 (Objective)
请你找到一种分配方案,将所有社区恰好分配给 num 个志愿者,使得耗时最长的那个志愿者的工作时间尽可能短。返回这个最短的工作时间。
解答要求
- 时间限制: 1000ms
- 内存限制: 256MB
输入格式
-
第一个参数
num:- 可用的志愿者数量。
1 <= num <= 10^5
-
第二个参数
communities:- 一个整数数组,表示每个社区的家庭数量。
1 <= communities.length <= 10^51 <= communities[i] <= 10^4
输出格式
- 一个整数,表示完成所有配送任务所需的最少小时数。
样例
输入样例 1
2
[40, 10, 20]
输出样例 1
40
解释:
有 2 名志愿者和 3 个社区 [40, 10, 20]。最优的分配方案是:
-
志愿者 1: 负责
{社区0},耗时 =40小时。 -
志愿者 2: 负责 {社区1, 社区2},耗时 = 10 + 20 = 30 小时。
所有志愿者完成工作的最长时间是 max(40, 30) = 40 小时。这是所有分配方案中能达到的最短总时间。
输入样例 2
2
[1, 1, 6, 2]
输出样例 2
8
解释:
有 2 名志愿者和 4 个社区 [1, 1, 6, 2]。
-
方案 A: {1, 1} | {6, 2}
- 志愿者 1 耗时:
1 + 1 = 2 - 志愿者 2 耗时:
6 + 2 = 8 - 总耗时:
max(2, 8) = 8小时。
- 志愿者 1 耗时:
-
方案 B: {1, 1, 6} | {2}
- 志愿者 1 耗时:
1 + 1 + 6 = 8 - 志愿者 2 耗时:
2 - 总耗时:
max(8, 2) = 8小时。
- 志愿者 1 耗时:
对比所有可能的分配方案,最短的完成时间是 8 小时。
输入样例 3
3
[1, 2]
输出样例 3
2
解释:
有 3 名志愿者和 2 个社区。因为每个社区只能分配一名志愿者,所以最多只能派出 2 名志愿者。
-
志愿者 1: 负责
{社区0},耗时 =1小时。 -
志愿者 2: 负责
{社区1},耗时 =2小时。 -
志愿者 3: 空闲。
总耗时 = max(1, 2) = 2 小时。
/**
* 解决社区蔬菜配送问题的实现类.
* 核心思想是使用“二分查找答案”来找到最优解。
*/
public class CommunityDelivery {
/**
* 主方法,计算在给定志愿者数量下,完成所有社区配送任务所需的最短时间。
*
* @param num 可用的志愿者数量
* @param communities 一个数组,其中 communities[i] 表示第 i 个社区的家庭数量(即所需配送小时数)
* @return 完成所有任务所需的最少小时数
*/
public int findMinTime(int num, int[] communities) {
// --- 步骤 1: 确定二分查找的范围 ---
long totalHours = 0; // 所有社区的总工作量,使用long防止溢出
int maxSingleCommunityHours = 0; // 单个社区的最大工作量
for (int hours : communities) {
totalHours += hours;
maxSingleCommunityHours = Math.max(maxSingleCommunityHours, hours);
}
// 搜索范围的下界(left):
// 至少需要的时间是处理最大单个社区所需的时间,因为一个社区不能拆分给多个志愿者。
long left = maxSingleCommunityHours;
// 搜索范围的上界(right):
// 最坏的情况下,一个志愿者处理所有社区,所需时间是所有社区工作量之和。
long right = totalHours;
// 用于存储最终找到的最小可行时间
long minTimeResult = right;
// --- 步骤 2: 执行二分查找 ---
while (left <= right) {
// 计算中间值作为本次猜测的“最短完成时间”
long midTime = left + (right - left) / 2;
// 检查使用 num 个志愿者,是否能在 midTime 小时内完成所有任务
if (canFinish(midTime, num, communities)) {
// 如果可以完成,说明 midTime 是一个可行的解。
// 我们记录下这个解,并尝试寻找一个更小的时间。
minTimeResult = midTime;
right = midTime - 1;
} else {
// 如果不能完成,说明 midTime 这个时间太短了,需要增加时间。
left = midTime + 1;
}
}
return (int) minTimeResult;
}
/**
* 辅助方法:检查在给定的时间限制(timeLimit)下,是否能用指定数量的志愿者(numVolunteers)完成任务。
*
* @param timeLimit 每个志愿者工作的最大时间限制
* @param numVolunteers 可用的志愿者数量
* @param communities 社区工作量数组
* @return 如果可行,返回 true;否则返回 false
*/
private boolean canFinish(long timeLimit, int numVolunteers, int[] communities) {
// 需要的志愿者数量,初始为1(至少需要一个人开始工作)
int volunteersNeeded = 1;
// 当前这个志愿者已经分配的工作量
long currentVolunteerWorkload = 0;
// 使用贪心策略,按顺序为志愿者分配连续的社区
for (int communityHours : communities) {
// 如果单个社区的工作量就超过了时间限制,那么这个时间限制是绝对不可能实现的。
// (虽然我们的二分查找下界已经保证了这种情况不会发生,但这是一个稳健性检查)
if (communityHours > timeLimit) {
return false;
}
// 尝试将当前社区分配给正在工作的志愿者
if (currentVolunteerWorkload + communityHours <= timeLimit) {
// 如果没超时,就累加工作量
currentVolunteerWorkload += communityHours;
} else {
// 如果超时了,就需要一个新的志愿者
volunteersNeeded++;
// 这个新志愿者的工作量从当前这个社区开始计算
currentVolunteerWorkload = communityHours;
}
}
// 循环结束后,比较需要的志愿者数量和可用的志愿者数量
return volunteersNeeded <= numVolunteers;
}
}