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)

[洛谷P1113]杂务

二分法+回溯+剪枝解决工人完成所有工作最短时间问题

启动嵌入式 Jetty 服务器的最短代码