1723. 完成所有工作的最短时间
Posted Zephyr丶J
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了1723. 完成所有工作的最短时间相关的知识,希望对你有一定的参考价值。
1723. 完成所有工作的最短时间
2021.5.8每日一题
题目描述
给你一个整数数组 jobs ,其中 jobs[i] 是完成第 i 项工作要花费的时间。
请你将这些工作分配给 k 位工人。所有工作都应该分配给工人,且每项工作只能分配给一位工人。工人的 工作时间 是完成分配给他们的所有工作花费时间的总和。请你设计一套最佳的工作分配方案,使工人的 最大工作时间 得以 最小化 。
返回分配方案中尽可能 最小 的 最大工作时间 。
示例 1:
输入:jobs = [3,2,3], k = 3
输出:3
解释:给每位工人分配一项工作,最大工作时间是 3 。
示例 2:
输入:jobs = [1,2,4,7,8], k = 2
输出:11
解释:按下述方式分配工作:
1 号工人:1、2、8(工作时间 = 1 + 2 + 8 = 11)
2 号工人:4、7(工作时间 = 4 + 7 = 11)
最大工作时间是 11 。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/find-minimum-time-to-finish-all-jobs
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
思路
首先二分查找确定每个工人的操作时间,然后固定时间以后,分配 i 个工作给 j 个工人,
分配工作的时候,从时间长的工作开始分配,按照我下边的思路先写出来下面的代码
class Solution {
int[] jobs;
int k;
public int minimumTimeRequired(int[] jobs, int k) {
//就是那啥呗,二分查找呗好像,left是数组中的最大数,right是总和,然后查找一个数,使得在这个时间下,能完成所有任务,
//但是怎么写呢,不太好写感觉。
//主要是怎么分配任务,而又保证所有情况都能考虑到,这个就感觉...
//首先分配时间长的工作,将时间长的工作挨个分配,比如[1,2,4,7,8], k = 2
//如果此时查找到的数是12,先将8分配给工人x,7分配给y,然后4可以分配给x,那么就分配给x
//然后依次分配,好像可行,那么right减少,继续分配,写一下代码
this.jobs = jobs;
this.k = k;
Arrays.sort(jobs); //先排序
int l = jobs.length;
int left = jobs[l - 1];
int right = Arrays.stream(jobs).sum();
//k个工人的时间
while(left < right){
int mid = (right - left) / 2 + left;
//分配所有的工作
boolean flag = true; //是否分配成功
int[] time = new int[k];
for(int i = l - 1; i >= 0; i--){
//分配第i个工作
if(!distribute(time, mid, i))
flag = false;
}
if(!flag){
left = mid + 1;
}else{
right = mid;
}
}
return left;
}
public boolean distribute(int[] time, int mid, int i){
for(int j = k - 1; j >= 0; j--){
if(time[j] + jobs[i] <= mid){
time[j] = time[j] + jobs[i];
return true;
}
}
return false;
}
}
然后发现过了53个测试用例后挂了, [6518448,8819833,7991995,7454298,2087579,380625,4031400,2905811,4901241,8480231,7750692,3544254] 4 这个示例没过
看了一下,发现这样给工人挑工作有问题,怎么说呢
就是我是按照从大到小依次给工人选工作,如果没有超过当前限制,就选取该工作,那么就会造成一个情况,例如当前限制是15,然后我选了最大的8 和 6,然后后面的工作一个也选不了了,然后导致最终返回false;但是有可能会发生其实可以选8 4 3 三个工作使当前工人的工作时间最大,然后恰好后面的工作时间就可以都安排好了,所以这种贪心的选择方法会使最终结果偏大
所以为了将所有情况都能考虑进去,还是得回溯
回溯中要注意两个可以剪枝的地方,因为所有工人都是一样的,
所以第一个剪枝点就是如果当前工人安排工作后时间为0,那么就是0
第二个点,就是如果当前工人安排工作后时间满了(已是最优的情况),而递归返回false,就相当于固定一个人的工作(这个人和这组工作不需要再考虑了),然后给其他人分配其他工作,递归进去不可行,就说明这个方案不行,所以直接false
剪枝的地方有点绕,好好想一下
最终代码:
class Solution {
int[] jobs;
int k;
public int minimumTimeRequired(int[] jobs, int k) {
//就是那啥呗,二分查找呗好像,left是数组中的最大数,right是总和,然后查找一个数,使得在这个时间下,能完成所有任务,
//但是怎么写呢,不太好写感觉。
//主要是怎么分配任务,而又保证所有情况都能考虑到,这个就感觉...
//首先分配时间长的工作,将时间长的工作挨个分配,比如[1,2,4,7,8], k = 2
//如果此时查找到的数是12,先将8分配给工人x,7分配给y,然后4可以分配给x,那么就分配给x
//然后依次分配,好像可行,那么right减少,继续分配,写一下代码
//这样写了以后发现不可行,主要问题是啥呢,就是没把所有的情况考虑进去,不能这样匹配工人的时间,而是应该整体考虑所有人的时间
//所以还是回溯吧
this.jobs = jobs;
this.k = k;
Arrays.sort(jobs); //先排序
int l = jobs.length;
int left = jobs[l - 1];
int right = Arrays.stream(jobs).sum();
//k个工人的时间
while(left < right){
int mid = (right - left) / 2 + left;
//分配所有的工作
boolean flag = true; //是否分配成功
int[] time = new int[k];
//从第l - 1个工作开始分配
flag = distribute(time, mid, l - 1);
if(!flag){
left = mid + 1;
}else{
right = mid;
}
}
return left;
}
public boolean distribute(int[] time, int mid, int i){
if(i < 0) //如果都分配了就可行
return true;
//给第j个工人 分配第i个工作
for(int j = 0; j < k; j++){
if(time[j] + jobs[i] <= mid){
time[j] += jobs[i];
//如果可行就分配下一个工作
if(distribute(time, mid, i - 1))
return true;
//不行回溯
time[j] -= jobs[i];
}
//如果当前工人分配工作后时间为0,那么就说明没有分配工作,那么下一个工人也肯定不会有工作了,所以直接break
//另外,如果当前工人的工作时间满了,并且递归进去也没有分配好,那么不需要继续分了,
//因为此时相当于固定了一个人的工作,再分配其他工作给其他人,而递归进去发现不可行,说明这个方案不行,所以也直接跳出
if(time[j] == 0 || time[j] + jobs[i] == mid){
break;
}
}
return false;
}
}
然后日常看了一下三叶姐的题解,在最简单的dfs上加了一个,优先分配工作给空闲的工人,就达到了很好的效果;另外还用了模拟退火,只能说tql,因为做课题基本只是提到过模拟退火,没有具体用过,这里又学习一波,什么时候才能达到三叶姐的水平!!!!
贴代码学习一下
class Solution {
int[] jobs;
int n, k;
int ans = 0x3f3f3f3f;
public int minimumTimeRequired(int[] _jobs, int _k) {
jobs = _jobs;
n = jobs.length;
k = _k;
int[] sum = new int[k];
dfs(0, 0, sum, 0);
return ans;
}
/**
* u : 当前处理到那个 job
* used : 当前分配给了多少个工人了
* sum : 工人的分配情况 例如:sum[0] = x 代表 0 号工人工作量为 x
* max : 当前的「最大工作时间」
*/
void dfs(int u, int used, int[] sum, int max) {
if (max >= ans) return;
if (u == n) {
ans = max;
return;
}
// 优先分配给「空闲工人」
if (used < k) {
sum[used] = jobs[u];
dfs(u + 1, used + 1, sum, Math.max(sum[used], max));
sum[used] = 0;
}
for (int i = 0; i < used; i++) {
sum[i] += jobs[u];
dfs(u + 1, used, sum, Math.max(sum[i], max));
sum[i] -= jobs[u];
}
}
}
作者:AC_OIer
链接:https://leetcode-cn.com/problems/find-minimum-time-to-finish-all-jobs/solution/gong-shui-san-xie-yi-ti-shuang-jie-jian-4epdd/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
模拟退火
因为将 n 个数划分为 k 份,等效于用 n 个数构造出一个「特定排列」,然后对「特定排列」进行固定模式的任务分配逻辑,就能实现「答案」与「最优排列」的对应关系。
基于此,我们可以使用「模拟退火」进行求解。
单次迭代的基本流程:
随机选择两个下标,计算「交换下标元素前对应序列的得分」&「交换下标元素后对应序列的得分」
如果温度下降(交换后的序列更优),进入下一次迭代
如果温度上升(交换前的序列更优),以「一定的概率」恢复现场(再交换回来)
class Solution {
int[] jobs;
int[] works = new int[20];
int n, k;
int ans = 0x3f3f3f3f;
Random random = new Random(20210508);
// 最高温/最低温/变化速率(以什么速度进行退火,系数越低退火越快,迭代次数越少,落入「局部最优」(WA)的概率越高;系数越高 TLE 风险越大)
double hi = 1e4, lo = 1e-4, fa = 0.90;
// 迭代次数,与变化速率同理
int N = 400;
// 计算当前 jobs 序列对应的最小「最大工作时间 」是多少
int calc() {
Arrays.fill(works, 0);
for (int i = 0; i < n; i++) {
// [固定模式分配逻辑] : 每次都找最小的 worker 去分配
int idx = 0, cur = works[idx];
for (int j = 0; j < k; j++) {
if (works[j] < cur) {
cur = works[j];
idx = j;
}
}
works[idx] += jobs[i];
}
int cur = 0;
for (int i = 0; i < k; i++) cur = Math.max(cur, works[i]);
ans = Math.min(ans, cur);
return cur;
}
void swap(int[] arr, int i, int j) {
int c = arr[i];
arr[i] = arr[j];
arr[j] = c;
}
void sa() {
for (double t = hi; t > lo; t *= fa) {
int a = random.nextInt(n), b = random.nextInt(n);
int prev = calc(); // 退火前
swap(jobs, a, b);
int cur = calc(); // 退火后
int diff = prev - cur;
// 退火为负收益(温度上升),以一定概率回退现场
if (Math.log(diff / t) < random.nextDouble()) {
swap(jobs, a, b);
}
}
}
public int minimumTimeRequired(int[] _jobs, int _k) {
jobs = _jobs;
n = jobs.length;
k = _k;
while (N-- > 0) sa();
return ans;
}
}
作者:AC_OIer
链接:https://leetcode-cn.com/problems/find-minimum-time-to-finish-all-jobs/solution/gong-shui-san-xie-yi-ti-shuang-jie-jian-4epdd/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
动态规划+状态压缩
这个也很巧妙,今天没时间写了,就看一下,理解一下吧
因为工作数目比较少,所以用一个二进制数的位表示每一个工作是否有人做了(状态压缩)
然后d[i][j]表示前 i 个人完成了状态 j 的工作,所需要的最短时间
然后转移方程就是当第i个工人完成了 j 中所有工作的子集 j’ 所用时间的最小值
直接贴官解的代码吧,估计自己写也写不出来,加了点注释,方便理解
class Solution {
public int minimumTimeRequired(int[] jobs, int k) {
int n = jobs.length;
int[] sum = new int[1 << n];
//先处理sum[i]
//用于计算sum[i],也就是二进制位对应1时,所用的工作时间相加
for (int i = 1; i < (1 << n); i++) {
//返回i最右位1后面的0的数目
int x = Integer.numberOfTrailingZeros(i), y = i - (1 << x);
sum[i] = sum[y] + jobs[x];
}
//初始化为最大值
int[][] dp = new int[k][1 << n];
for (int i = 0; i < (1 << n); i++) {
dp[0][i] = sum[i];
}
//i个工人,j种工作情况
for (int i = 1; i < k; i++) {
for (int j = 0; j < (1 << n); j++) {
int minn = Integer.MAX_VALUE;
//讨论所有工作情况,(x - 1)会把x最右边的1变成0,后面补1,
//与J相与就会把x最右边的1变成0,后面也都是0,也就是有1位 发生变化
//这样不断的减1可以取到j的所有子集
for (int x = j; x != 0; x = (x - 1) & j) {
minn = Math.min(minn, Math.max(dp[i - 1][j - x], sum[x]));
}
dp[i][j] = minn;
}
}
return dp[k - 1][(1 << n) - 1];
}
}
作者:LeetCode-Solution
链接:https://leetcode-cn.com/problems/find-minimum-time-to-finish-all-jobs/solution/wan-cheng-suo-you-gong-zuo-de-zui-duan-s-hrhu/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
总结一下,回溯还得好好练,任重而道远
以上是关于1723. 完成所有工作的最短时间的主要内容,如果未能解决你的问题,请参考以下文章
LeetCode1723. 完成所有工作的最短时间 Java回溯+剪枝(详解)
LeetCode 1723 完成所有工作的最短时间[二分法 回溯] HERODING的LeetCode之路
LeetCode 1723. 完成所有工作的最短时间 Find Minimum Time to Finish All Jobs(Java)