最佳蔬菜配送方案

184 阅读5分钟

最佳蔬菜配送方案

背景介绍

疫情期间,需要组织志愿者为一批社区的家庭配送爱心蔬菜。每个社区的家庭数量不同,而每个志愿者的时间和精力是有限的。为了尽快完成任务,需要你设计一个最高效的分配方案。

核心概念

  1. 社区 (communities):

    • 这是一个按顺序排列的社区列表。
    • communities[i] 表示第 i 个社区的家庭数量。
  2. 志愿者 (Volunteer):

    • 共有 num 名志愿者可以同时开始工作(并行配送)。
  3. 配送任务 (Delivery Task):

    • 每个志愿者负责配送连续的若干个社区。例如,可以负责 [社区0, 社区1, 社区2],但不能跳着负责 [社区0, 社区2]
    • 每个社区只能由一名志愿者负责。
  4. 耗时计算 (Time Calculation):

    • 为每个家庭配送耗时均为 1 小时。
    • 单个志愿者的耗时 = 他所负责的所有社区的家庭数量之和。
    • 总任务耗时 = 所有志愿者中,那个耗时最长的人所花的时间。因为所有人是并行工作的,最终的完成时间取决于最慢的那个人。

目标 (Objective)

请你找到一种分配方案,将所有社区恰好分配给 num 个志愿者,使得耗时最长的那个志愿者的工作时间尽可能短。返回这个最短的工作时间。


解答要求

  • 时间限制: 1000ms
  • 内存限制: 256MB

输入格式

  1. 第一个参数 num:

    • 可用的志愿者数量。
    • 1 <= num <= 10^5
  2. 第二个参数 communities:

    • 一个整数数组,表示每个社区的家庭数量。
    • 1 <= communities.length <= 10^5
    • 1 <= 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 小时。
  • 方案 B: {1, 1, 6} | {2}

    • 志愿者 1 耗时: 1 + 1 + 6 = 8
    • 志愿者 2 耗时: 2
    • 总耗时: max(8, 2) = 8 小时。

对比所有可能的分配方案,最短的完成时间是 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;
    }
}