【OD机试解法笔记】项目排期

项目组共有N个开发人员,项目经理接到了M个独立的需求,每个需求的工作量不同,且每个需求只能由一个开发人员独立完成,不能多人合作。假定各个需求直接无任何先后
依赖关系,请设计算法帮助项目经理进行工作安排,使整个项目能用最少的时间交付。
输入描述
第一行输入为M个需求的工作量,单位为天,用逗号隔开。
例如:X1 X2 X3 … Xm 。表示共有M个需求,每个需求的工作量分别为X1天,X2天…Xm天。
其中0<M<30;0<Xm<200
第二行输入为项目组人员数量N
输出描述
最快完成所有工作的天数

用例

输入输出说明
6 2 7 7 9 3 2 1 3 11 4
2
28共有两位员工,其中一位分配需求 6 2 7 7 3 2 1共需要28天完成,另一位分配需求 9 3 11 4 共需要27天完成,故完成所有工作至少需要28天。

思考

       将 N 个员工 抽象为 N 条并行流水线,每条流水线独立处理分配的需求,流水线的处理时间为分配需求的工作量之和。目标是将 M 个需求 分配至 N 条流水线,使得 所有流水线处理时间的最大值最小(即「最长流水线时间」最短)。可以理解成并行流水线的负载均衡问题。那么容易想到的解法是暴力求解组合数,假如为第一个员工选取 k (0 <= k <= M)个需求,则第二个员工只能从 M - k 个需求中继续选。写代码时遍历需求列表 works,对当前需求 work[i] 给出分配或不分配给员工 work[i] 的两种决策,用递归和回溯实现。对于所有组合数求出每种员工的花费的天数,取出员工时间最大值不断更新全局最小的完成需求天数。时间复杂度是指数级别,对于题目给出的规模 0<M<30;0<Xm<200 可能会超时。

       利用二分查找最少完成需求天数+回溯分配任务判断可行性能降低复杂度, 可以预先对工作量进行降序排序(优先分配大任务),利用贪心策略优化,这能显著提高回溯算法的效率。

算法过程

  1. 输入处理:读取任务列表和工人数量,处理边界情况(工人数量为 0 时直接返回 0)。

  2. 任务排序:将任务按工作量降序排列,优先处理大任务以提高回溯剪枝效率。

  3. 二分查找框架

    • 初始化左边界 l 为最大单个任务量,右边界 r 为所有任务的工作量总和。
    • 在 [l, r) 范围内进行二分查找,每次取中间值 mid 作为候选最大时间限制。
  4. 可行性验证(回溯算法)

    • 尝试将所有任务分配给工人,使得每个工人的总工作量不超过 mid
    • 使用回溯法递归分配任务,剪枝条件:
      • 若当前任务无法分配给任何工人,立即返回失败。
      • 若某工人分配任务后总工作量达到 mid,不再尝试后续工人(避免对称分配)。
      • 若某工人当前无任务,且分配当前任务后无法完成,后续工人无需尝试(避免重复路径)。
  5. 调整边界

    • 若存在可行分配方案,缩小右边界 r = mid
    • 若不存在,增大左边界 l = mid + 1
  6. 输出结果:二分查找结束时,l 即为最小的最大完成时间。

时间复杂度分析

  • 排序阶段:对 M 个任务进行降序排序,时间复杂度为 \(O(M \log M)\)。

  • 二分查找阶段

    • 二分查找的区间为 [max_task, sum_tasks],时间复杂度为 \(O(\log \text{sum\_tasks})\)。
    • 每次二分需要调用回溯算法验证可行性。
  • 回溯验证阶段

    • 回溯算法的理论时间复杂度为 \(O(N^M)\)(每个任务有 N 种分配可能)。
    • 但通过降序排序和剪枝优化,实际复杂度显著降低,接近 \(O(M \times N)\)。
  • 总体时间复杂度:\(O(M \log M + \log(\text{sum\_tasks}) \times f(M,N))\),其中 \(f(M,N)\) 为剪枝后的回溯复杂度,通常远小于 \(O(N^M)\)。

参考代码

function solution() {
  const tasks = readline().split(" ").map(Number);
  const n = parseInt(readline());
  if (n === 0) return 0;

  // 对工作量进行降序排序,优先处理大任务
  tasks.sort((a, b) => b - a);

  const backtrack = function(workers, index, limit) {
    // 如果所有任务都已分配,则返回true
    if (index >= tasks.length) {
      return true;
    }

    // 获取当前任务的工作量
    let current = tasks[index];
    // 尝试将当前任务分配给每个员工
    for (let i  = 0; i < workers.length; i++) {
      if (workers[i] + current <= limit) {
        // 分配任务给当前员工
        workers[i] += current;
        // 继续尝试分配下一个任务
        if (backtrack(workers, index + 1, limit)) {
          return true;
        }
        // 回溯,取消当前任务分配
        workers[i] -= current;
      }

      // 如果当前员工没有任务或者加上当前任务刚好达到时间限制,则不需要尝试其他员工
      if (workers[i] === 0 || workers[i] + current === limit) {
        break;
      }
    }

    return false;
  };

  const canFinish = function(limit) {
    // 创建一个数组记录每个员工的工作量
    let workers = new Array(n).fill(0);
    // 使用回溯法检查是否可以完成
    return backtrack(workers, 0, limit);
  };

  let l = tasks[0], r = tasks.reduce((a,b)=>a+b, 0);
  while (l < r) {
    let mid = l + Math.floor((r -l)/2);
    if (canFinish(mid)) {
      r = mid;
    } else {
      l = mid +1;
    }
  }
 
  console.log(l);
}

const cases = [
  `6 2 7 7 9 3 2 1 3 11 4
  2`,
];

let caseIndex = 0;
let lineIndex = 0;

const readline = (function () {
  let lines = [];
  return function () {
    if (lineIndex === 0) {
      lines = cases[caseIndex]
        .trim()
        .split("\n")
        .map((line) => line.trim());
    }
    return lines[lineIndex++];
  };
})();

cases.forEach((_, i) => {
  caseIndex = i;
  lineIndex = 0;
  solution();
});

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值