原题描述
LeetCode第1723题 完成所有工作的最短时间
难度:困难
给你一个整数数组jobs
,其中 jobs[i]
是完成第i
项工作要花费的时间。
请你将这些工作分配给 k
位工人。所有工作都应该分配给工人,且每项工作只能分配给一位工人。工人的 工作时间 是完成分配给他们的所有工作花费时间的总和。请你设计一套最佳的工作分配方案,使工人的 最大工作时间 得以 最小化 。
返回分配方案中尽可能 最小 的 最大工作时间
我的思路
错误思路
我最初打算使用贪心算法来解决此问题,后来发现该方法并不能得到最终解。贪心算法的大致思路为:
- 将
jobs
按照从大到小的顺序排序; - 依次从
jobs
中取出未分配的最大工作时间; - 将第2步取出的工作分配给当前工作时间最短的人。
本思路下的尝试
class Solution {
public int minimumTimeRequired(int[] jobs, int k) {
Arrays.sort(jobs);
int[] time = new int[k];
int minIndex = 0;
for (int i = jobs.length - 1; i >= 0; i--) {
time[minIndex] += jobs[i];
int minTime = time[minIndex];
for (int j = 0; j < k; j++) {
if (time[j] < minTime){
minIndex = j;
}
}
}
int max = 0;
for (int i = 0; i < k; i++) {
max = Math.max(max,time[i]);
}
return max;
}
}
提交结果:解答错误
输入:
[5,5,4,4,4]
2
预期输出:12
实际输出:13
分析原因
通过debug可以得知,上面代码中的time
数组的变化情况为[5,0]
→ [5,5]
→ [9,5]
→ [9,9]
→ [13,9]
并得出最后结论为13。但是更优的方案为[5+5,4+4+4]
,其结果为12。这说明贪心算法的思路并不正确。
正确思路
正确思路非常直接——DFS暴力遍历。其具体流程为:
- 将第1份工作分配给第1个人;
- 然后开始使用DFS进行遍历:
2.1. 如果已经分配到最后一项工作,则计算当前状态下的最大工作时间,并返回结果;
2.2. 否则将下一份工作尝试给每个人分配一次,并对它们的返回值进行比较,选取其中最小的一个作为结果返回;
我的尝试
第一次
class Solution {
public int minimumTimeRequired(int[] jobs, int k) {
return dfs(jobs,new int[k],0,0);
}
private int dfs(int[] job, int[] times, int index, int who){
times[who] += job[index];
if (index == job.length - 1){
// 求最大值
int max = 0;
for (int time : times) {
max = Math.max(time, max);
}
// 清理数组
times[who] -= job[index];
return max;
}
// 求最小值
int min = Integer.MAX_VALUE;
for (int i = 0; i < times.length; i++) {
int ret = dfs(job, times, index + 1, i);
min = Math.min(ret,min);
}
// 清理数组
times[who] -= job[index];
return min;
}
}
提交结果:超出时间限制
输入:
[9899456,8291115,9477657,9288480,5146275,7697968,8573153,3582365,3758448,9881935,2420271,4542202]
9
样例运行至:16/60
分析原因
代码的时间复杂度过高,也说明代码中存在大量重复计算。
以第1次进入dfs()
函数为例,此时times
中存在这大量的0元素,他们是等价的,下一个工作分配到哪一个0元素都一样,因此,对于times
中同样的时间,我们仅需要尝试1次而不必多次尝试。
解决办法
在执行2.2之前,可以使用一个map存储times
中出现的值以及相应的坐标,然后仅需要对map中存储的坐标进行遍历即可,进而避免重复运算。
第二次
class Solution {
public int minimumTimeRequired(int[] jobs, int k) {
return dfs(jobs,new int[k],0,0);
}
private int dfs(int[] job, int[] times, int index, int who){
//System.out.println("index = " + index+" : who = " + who);
times[who] += job[index];
if (index == job.length - 1){
int max = 0;
for (int time : times) {
max = Math.max(time, max);
}
// 清理数组
times[who] -= job[index];
return max;
}
// map存储时间,避免重复
HashMap<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < times.length; i++) {
map.putIfAbsent(times[i],i);
}
int min = Integer.MAX_VALUE;
for (int i : map.values()) {
int ret = dfs(job, times, index + 1, i);
min = Math.min(ret,min);
}
times[who] -= job[index];
return min;
}
}
提交结果:超出时间限制
输入:
[643,526,589,976,986,730,345,926,798,618,827,873]
11
样例运行至:59/60
分析原因
代码的运行时间虽然大大改进,但是依然存在浪费计算量的情况,也就是说,代码中可能存在无效计算。
例如,我们在上面的样例中已经得到一组分配方式[643,526,589,976,986,730+345,926,798,618,827,873]
,其结果为730+345=1075,那么在执行到[643,526,589,976,986,730,345,0,0,0,0]
时,下一个将要分配的工作工作时间为926,那么它无需分配到643上,因为此时926+643>1075,此方案必然不会成为最终的方案,因此此时可以提前中止执行dfs()
函数。
解决方案
在执行第2步的最开始,我们可以先判断分配之后的时间长度是否不小于了当前已知的最优解,如果它没有最优解小,这可以提前中止递归,直接返回。同时,由于需要记录全局最优解,因此需要在类中定义一个变量存放最优解。此外,在第2步的比较中,第2.1步的比较结果可以直接更新到全局最优解中,第2.2步的比较也可以去掉。
第三次
class Solution {
int min = Integer.MAX_VALUE;
public int minimumTimeRequired(int[] jobs, int k) {
dfs(jobs,new int[k],0,0);
return min;
}
private void dfs(int[] job, int[] times, int index, int who){
//System.out.println("index = " + index+" : who = " + who);
times[who] += job[index];
if (times[who] >= min){
// 清理数组
times[who] -= job[index];
return;
}
if (index == job.length - 1){
int max = 0;
for (int time : times) {
max = Math.max(time, max);
}
min = Math.min(min,max);
// 清理数组
times[who] -= job[index];
return;
}
HashMap<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < times.length; i++) {
map.putIfAbsent(times[i],i);
}
for (int i : map.values()) {
dfs(job, times, index + 1, i);
}
times[who] -= job[index];
}
}
提交结果:通过