有LeetCode算法/华为OD考试扣扣交流群可加 948025485
可上全网独家的 欧弟OJ系统 练习华子OD、大厂真题
绿色聊天软件戳od1336
了解算法冲刺训练
题目描述与示例
题目描述
项目组共有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
天。
解题思路
又是一道描述相当晦涩的题目(很想骂人)
用比较简洁的数学语言来描述就是,将数组X1, X2, X3, ... ,Xm
分为N
部分(并非子数组,不要求连续),设每一部分的和为sum1, sum2, ..., sumN
,要求找到一种分配方式使得max(sum1, sum2, ..., sumN)
最小。
用一句简单的话来说,就是最小化各部分求和的最大值。这种设问一定要想到用二分来完成。
将问题转化为,我们需要找到一个阈值k
(一个人的最大工作量),并将原数组nums
可以被分为N
部分(分配给N
个人),使得这N
部分的各自求和的最大值都不会超过k
(每个人各自的工作量不会超过k
)。
显然k
的选择是一个二段性问题:
- 当
k
非常大时,原数组nums
无论如何分配都可以完成要求 - 当
k
非常小时,原数组nums
无论怎么分配都无法完成要求 - 必然存在一个临界值
k
,使得存在nums
的分配结果恰好完成要求。
我们希望找到这个阈值k
,因此需要对k
进行二分查找,二分查找的范围为[max(nums), sum(nums)]
。当
k = max(nums)
时,能够被分为m = len(nums)
部分(需要m
个人来完成所有工作)k = sum(nums)
时,能够被分为1
部分(只由1
个人可以完成所有工作)
而上述二分过程的贪心子问题为:当我们选择了阈值k
时,数组nums
能否被分割不超过N
部分?
这个问题就和【贪心】2023B-数据最节约的备份方法几乎完全一致了,其代码为
def sub_question(k, nums, m, N):
ans = 0
check = [0] * m
for i in range(m):
if check[i] == 1:
continue
ans += 1
cur_sum = 0
j = i
while j < m:
if check[j] == 1:
j += 1
continue
if nums[j] + cur_sum > k:
j += 1
else:
cur_sum += nums[j]
check[j] = 1
j += 1
return ans <= N
初始化左闭右开区间left = max(nums)
,right = sum(nums) + 1
,进行二分。
计算mid = (left + right) // 2
。当
sub_question(mid, nums, m, N)
为True
时,说明当选择了阈值k = mid
时,可以将任务分配给N
个人(组数小于等于N
)。此时的mid
的选择偏大,区间应该左移,right
左移。sub_question(mid, nums, m, N)
为False
时,说明当选择了阈值k = mid
时,无法将任务分配给N
个人(组数大于N
)。此时的mid
的选择偏小,区间应该右移,left
右移。
故结合贪心子问题,整体的二分代码为
nums = list(map(int, input().split()))
m = len(nums)
N = int(input())
nums.sort(reverse = True)
left, right = max(nums), sum(nums)+1
while left < right:
mid = (right + left) // 2
if sub_question(mid, nums, m, N):
right = mid
else:
left = mid + 1
print(left)
代码
python
# 题目:【二分查找】2023C-项目排期
# 分值:200
# 作者:许老师-闭着眼睛学数理化
# 算法:二分查找/贪心
# 代码看不懂的地方,请直接在群上提问
# 相关题目:【贪心】2023B-数据最节约的备份方法
# 贪心子问题,当选择了阈值k时,如果m个任务nums可以被分配给N个员工,
# 且每一个员工的工作总量不超过k则返回True,否则返回False
# (注意此处nums必须是一个已排序好的逆序数组)
# 该子问题的时间复杂度与nums的长度m相关,为O(m^2)
def sub_question(k, nums, m, N):
ans = 0
# 初始化长度为m的check数组,用来表示第i个任务是否已经分配给了某一个员工
check = [0] * m
# 遍历所有nums每一个工作量
for i in range(m):
# 如果该工作已经由某个员工完成了,则直接跳过
if check[i] == 1:
continue
# 否则,需要一个新的员工
# 来完成包含工作nums[i]在内的若干工作
# 故ans更新
ans += 1
# 初始化当前员工所做的工作总量为0
cur_sum = 0
# 初始化变量j为i,用于修改当前这个员工的工作情况
j = i
# 进行内层循环,此处涉及贪心算法
while j < m:
# 如果第j个工作已经安排,则j直接递增,跳过第j个工作
if check[j] == 1:
j += 1
continue
# 如果第j份工作和当前员工之前的工作量之和超过k
# 则这个员工不能选择这份工作,j递增
if nums[j] + cur_sum > k:
j += 1
# 如果第j份工作和当前员工之前的工作量之和不超过k
# 则贪心地将这份工作安排给这个员工
# 修改cur_sum和check[j],j递增
else:
cur_sum += nums[j]
check[j] = 1
j += 1
# 退出循环时,如果需要的人数ans不超过N,则返回True,否则返回False
return ans <= N
# 输入m个任务构成的数组
nums = list(map(int, input().split()))
# 获得nums的长度,即任务数量
m = len(nums)
# 输入员工人数N
N = int(input())
# 对nums进行逆序排序,方便后续贪心子问题的计算
nums.sort(reverse = True)
# 初始化左闭右开
left, right = max(nums), sum(nums)+1
while left < right:
mid = (right + left) // 2
# 如果选择了mid作为阈值,可以将任务分配给N个人(组数小于等于N)
# 说明mid的选择偏大,区间应该左移,right左移
if sub_question(mid, nums, m, N):
right = mid
# 如果选择了mid作为阈值,无法将任务分配给N个人(组数多于N)
# 说明mid的选择偏小,区间应该右移,left右移
else:
left = mid + 1
# 退出循环时,存在left = right是恰好可以将任务分配给N个人的阈值k
# left或right即为答案
print(left)
java
import java.util.Arrays;
import java.util.Scanner;
public class Main {
// 贪心子问题
private static boolean subQuestion(int k, int[] nums, int m, int N) {
int ans = 0;
int[] check = new int[m];
for (int i = 0; i < m; i++) {
if (check[i] == 1) continue;
ans++;
int curSum = 0;
int j = i;
while (j < m) {
if (check[j] == 1) {
j++;
continue;
}
if (nums[j] + curSum > k) {
j++;
} else {
curSum += nums[j];
check[j] = 1;
j++;
}
}
}
return ans <= N;
}
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
// 输入m个任务构成的数组
String[] numsStr = scanner.nextLine().split(" ");
int m = numsStr.length;
int[] nums = new int[m];
for (int i = 0; i < m; i++) {
nums[i] = Integer.parseInt(numsStr[i]);
}
// 输入员工人数N
int N = scanner.nextInt();
// 对nums进行逆序排序
Arrays.sort(nums);
for (int i = 0; i < m / 2; i++) {
int temp = nums[i];
nums[i] = nums[m - i - 1];
nums[m - i - 1] = temp;
}
// 初始化左闭右开
int left = nums[0], right = Arrays.stream(nums).sum() + 1;
while (left < right) {
int mid = left + (right - left) / 2;
if (subQuestion(mid, nums, m, N)) {
right = mid;
} else {
left = mid + 1;
}
}
// 输出结果
System.out.println(left);
}
}
cpp
#include <iostream>
#include <vector>
#include <algorithm>
#include <sstream>
#include <numeric>
using namespace std;
bool subQuestion(int k, vector<int>& nums, int m, int N) {
int ans = 0;
vector<int> check(m, 0);
for (int i = 0; i < m; i++) {
if (check[i] == 1) continue;
ans++;
int curSum = 0;
int j = i;
while (j < m) {
if (check[j] == 1) {
j++;
continue;
}
if (nums[j] + curSum > k) {
j++;
} else {
curSum += nums[j];
check[j] = 1;
j++;
}
}
}
return ans <= N;
}
int main() {
vector<int> nums;
string input;
getline(cin, input);
stringstream ss(input);
int num;
while (ss >> num) {
nums.push_back(num);
}
int m = nums.size();
int N;
cin >> N;
sort(nums.begin(), nums.end(), greater<int>());
int left = nums[0], right = accumulate(nums.begin(), nums.end(), 0) + 1;
while (left < right) {
int mid = left + (right - left) / 2;
if (subQuestion(mid, nums, m, N)) {
right = mid;
} else {
left = mid + 1;
}
}
cout << left << endl;
return 0;
}
时空复杂度
时间复杂度:O(log(C)m^2)
。贪心子问题的时间复杂度为O(m^2)
,二分查找的时间复杂度为O(logC)
,其中C = sum(nums) - max(nums)
。
空间复杂度:O(m)
。贪心子问题需要构建长度为m
的check
数组。
华为OD算法/大厂面试高频题算法练习冲刺训练
-
华为OD算法/大厂面试高频题算法冲刺训练目前开始常态化报名!目前已服务300+同学成功上岸!
-
课程讲师为全网50w+粉丝编程博主@吴师兄学算法 以及小红书头部编程博主@闭着眼睛学数理化
-
每期人数维持在20人内,保证能够最大限度地满足到每一个同学的需求,达到和1v1同样的学习效果!
-
60+天陪伴式学习,40+直播课时,300+动画图解视频,300+LeetCode经典题,200+华为OD真题/大厂真题,还有简历修改、模拟面试、专属HR对接将为你解锁
-
可上全网独家的欧弟OJ系统练习华子OD、大厂真题
-
可查看链接 大厂真题汇总 & OD真题汇总(持续更新)
-
绿色聊天软件戳
od1336
了解更多