一、题目
项目组共有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天。
五、题解
- 问题解有单调性,在一个区间范围内。考虑二分法找可行解和最优解
- 验证解的正确性时尝试过贪心和动态规划,都没有得到最优解。暴搜 DFS可以找到最优解,但是明显效率不高
5.1 Java 实现
package org.stone.study.algo.ex202412;
import java.util.Arrays;
import java.util.Scanner;
public class ProjectScheduler {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int[] workLoads = Arrays.stream(sc.nextLine().split(" ")).mapToInt(Integer::parseInt).toArray();
int n = Integer.parseInt(sc.nextLine());
int ans = new ProjectScheduler().minCompletionTimeByDFS(workLoads, n);
// int ans = new ProjectScheduler().minCompletionTimeByDP(workLoads, n);
System.out.println(ans);
}
/**
* 二分查找最小的工期
* @param workLoads 每个任务的工作量
* @param n 工人数量
* @return
*/
public int minCompletionTimeByDFS(int[] workLoads, int n) {
// 最长的工作量给一个人做,作为项目整体工期的最小值
int left = Arrays.stream(workLoads).max().orElse(0);
// 工作量总和给一个人做,作为项目整体工期的最大值
int right = Arrays.stream(workLoads).sum();
while(left < right) {
int mid = left + (right - left) / 2;
if(check(workLoads, mid, n)) {
right = mid;
} else {
left = mid + 1;
}
}
return left;
}
/**
* 检查是否可以完成任务
* @param workLoads 每个任务的工作量
* @param limit 每个人最大的工作量,超过这个值,就需要另一个工人
* @param n 工人数量
* @return
*/
public boolean check(int[] workLoads, int limit, int n) {
int[] allocates = new int[n];
// 贪心算法没能解决所有 case,需要回溯算法遍历所有可能
return dfs(workLoads, 0, limit, allocates);
}
/**
* 深度优先搜索
* @param workLoads 每个任务的工作量
* @param i 当前任务的索引
* @param limit 每个人最大的工作量
* @param allocates 每个人已经分配的工作量
* @return
*/
private boolean dfs(int[] workLoads, int i, int limit, int[] allocates) {
if(i == workLoads.length) {
return true;
}
// 当前任务
int curWorkLoad = workLoads[i];
// 遍历每个人,找到一个合适的工人
for(int j = 0; j < allocates.length; j++) {
if (allocates[j] + curWorkLoad <= limit) {
allocates[j] += curWorkLoad;
if (dfs(workLoads, i + 1, limit, allocates)) {
return true;
}
allocates[j] -= curWorkLoad;
}
}
return false;
}
/**
* 动态规划解法,得不到最优解。这玩意儿是玄学,不确定原因
* @param tasks
* @param n
* @return
*/
public int minCompletionTimeByDP(int[] tasks, int n) {
int m = tasks.length;
int[] prefixSum = new int[m + 1];
// 计算前缀和,prefixSum[i] 表示前 i 个任务的总工作量
for (int i = 0; i < m; i++) {
prefixSum[i + 1] = prefixSum[i] + tasks[i];
}
// dp[i][j] 表示前 i 个任务分配给 j 个开发人员的最小化最大工作量
int[][] dp = new int[m + 1][n + 1];
for (int[] row : dp) {
Arrays.fill(row, Integer.MAX_VALUE);
}
// 初始条件:一个开发人员负责所有任务
for (int i = 0; i <= m; i++) {
dp[i][1] = prefixSum[i]; // 所有任务由一个开发者完成
}
// 填充 DP 表
for (int j = 2; j <= n; j++) { // j 个开发人员
for (int i = 1; i <= m; i++) { // i 个任务
for (int k = 0; k < i; k++) { // 划分点
int currentMaxWorkload = Math.max(dp[k][j - 1], prefixSum[i] - prefixSum[k]);
dp[i][j] = Math.min(dp[i][j], currentMaxWorkload);
}
}
}
return dp[m][n];
}
}
5.2 Python实现
# 二分找最小工期
def min_time_to_complete(tasks, n):
left, right = max(tasks), sum(tasks)
while left < right:
mid = (left + right) // 2
if check(tasks, n, mid):
right = mid
else:
left = mid + 1
return left
# DFS 暴搜所有的分配方案,看是否存在一种分配方案,使得所有任务在 limit 时间内完成
def check(tasks, n, limit):
allocated = [0] * (n)
return dfs(tasks, 0, limit, allocated)
# 对任务 i,尝试分配给每一个工人 j
def dfs(tasks, i, limit, allocated):
if i == len(tasks):
return True
cur_workload = tasks[i]
for j in range(n):
if allocated[j] + cur_workload <= limit:
allocated[j] += cur_workload
if dfs(tasks, i + 1, limit, allocated):
return True
allocated[j] -= cur_workload
return False
if __name__ == '__main__':
tasks = list(map(int, input().split()))
n = int(input())
print(min_time_to_complete(tasks, n))