[leetcode每日一题2021/5/8]1723. 完成所有工作的最短时间

该博客讨论了一道LeetCode上的难题,涉及工作分配给多位工人的最短时间优化。作者提出使用动态规划方法,通过状态转移方程来解决,同时介绍了如何优化计算过程,包括快速计算子集和枚举子集的技巧。虽然二分查找加回溯的解决方案更快,但动态规划仍然提供了一种有效的思路。
摘要由CSDN通过智能技术生成

题目来源于leetcode,解法和思路仅代表个人观点。传送门
难度:困难(思路是对了,但是想不到快速枚举子集的方法,超时没过)
时间:-
TAG:动态规划

题目

给你一个整数数组 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 。

提示:

1 <= k <= jobs.length <= 12
1 <= jobs[i] <= 107

思路

可以看到,1 <= k <= jobs.length <= 12,那么这题回溯应该可解。(但是如果完全不做任何处理的话,肯定超时(因为是困难题),所以还要考虑减枝的问题)。


如果之前做过类似的题目,可以知道,这题就是CPU调度问题

  • 如果使用贪心解法,能在很快的时间得到一个较为不错的解(但不一定最优)。
  • 题目要求的是最优解,那么就类似旅行商问题,需要用回溯/动态规划解法。

当时考虑回溯就是自顶向下+备忘录方式,所以打算使用动态规划自底向上解法(更快一些?)。
后面看到官方答案(二分+回溯+减枝),运行了一下还是二分快啊。(二分+回溯0ms,动归500ms)

动态规划

定义数组 d p [ i ] [ j ] dp[i][j] dp[i][j]
i i i表示考虑 [ 0 , i ] [0,i] [0,i]号工人。 0 ≤ i < k 0 \leq i \lt k 0i<k
j j j表示工人可以选取的工作。 0 ≤ j < 2 n 0\leq j \lt 2^n 0j<2n

j j j使用每一位代表jobs的选取情况。1为选中,0为不选。
例如在示例2中, n = 5 n=5 n=5
j = 7 j=7 j=7(二进制为111)表示选中jobs[0],jobs[1],jobs[2]。
j = 5 j=5 j=5(二进制为101)表示选中jobs[0],jobs[2]。

数组 d p [ i ] [ j ] dp[i][j] dp[i][j]表示考虑 [ 0 , i ] [0,i] [0,i]号工人,可以供选择的工作为 j j j,那么分配方案中尽可能最小的最大工作时间(子问题)。最后答案 d p [ k − 1 ] [ 2 n − 1 ] dp[k-1][2^n-1] dp[k1][2n1]即为原题目所求。

例如示例1中, n = 3 , k = 3 n=3,k=3 n=3k=3 d p [ 1 ] [ 5 ] dp[1][5] dp[1][5]就表示一共有2个工人,可以选择的工作为jobs[0],jobs[2],那么分配方案中最小的最大工作时间。
那么,最后答案 d p [ 2 ] [ 7 ] dp[2][7] dp[2][7]即为所求。


状态转移方程

当只有1个工人时,我们初始化边界条件 d p [ 0 ] [ j ] dp[0][j] dp[0][j]
d p [ 0 ] [ j ] dp[0][j] dp[0][j]就等于所有被选中的工作相加。
d p [ 0 ] [ j ] = ∑ i = 0 n ( j o b s [ i ]   i f   ( 2 i & j )   e l s e   0 ) dp[0][j] = \sum_{i=0}^n( jobs[i] ~ if ~(2^i\&j) ~else~0) dp[0][j]=i=0n(jobs[i] if (2i&j) else 0)


对于 d p [ i ] [ j ] dp[i][j] dp[i][j]我们需要枚举第 i i i号工人在 j j j情况下的选取情况。即枚举 j j j的子集p。

然后,计算max(第 i i i号工人所花费的时间,第 [ 0 , i − 1 ] [0,i-1] [0,i1]号工人在 p p p关于 j j j的补集 上花费最小时间)。

例如 j = 7 j=7 j=7 p = 4 p=4 p=4 j j j的一个子集。 q = 3 q=3 q=3 p p p关于 j j j的补集。

然后,对于第 i i i号工人的每种选取情况( j j j的每一个子集),我们取一个最小值。


简单地说,现在工人 i i i可以选取的情况为 j j j,那么工人 i i i j j j上随意取,剩下的交给工人 [ 0 , i − 1 ] [0,i-1] [0,i1]

优化

有两点优化。

  1. 当知道选取情况为 j j j时,快速知道 s u m = ∑ i = 0 n ( j o b s [ i ]   i f   ( 2 i & j )   e l s e   0 ) sum=\sum_{i=0}^n( jobs[i] ~ if ~(2^i\&j) ~else~0) sum=i=0n(jobs[i] if (2i&j) else 0)是多少。

例如在示例2中, j = 5 j=5 j=5,sum = jobs[0]+jobs[2] = 5。

  1. 快速枚举 j j j的子集。
求和打表

对于选取情况为 j j j,最简单的方法就是依次枚举 j j j的每一位,有1就表示选中。代码如下:

int nbit = pow(2,n);
for(int j=1;j<nbit;j++){
	for(int q=0;q<n;q++){
	    int mask = pow(2,q);
	    //如果第q位是选中的(选中为1)
	    if(mask & j){
	    	sum += jobs[q];
	    }
	}
}

但是这样时间复杂度就达到了O( 2 n ⋅ n 2^n \sdot n 2nn)。有没有办法更快计算呢?


我们能不能通过之前的计算,建立转移方程呢?
其实我们观察例如 j = 6 j=6 j=6(二进制为110)时,如果1102只变化一位,那么我们就可以通过selectJobs[6] = jobs[2] + selectJobs[0102]或者jobs[1] + selectJobs[1002]得到。

那么是否可以找到只变化一位的方法呢?这里需要靠点经验了。
我们先需要知道 x & (x-1)的含义是什么?

x & (x-1)就是消去x中最低位的1。

那么,y = j - x 就是被消去的最低位。

例如, j = 6 j=6 j=6
x = 6 & 5 = 11 0 2 & 10 1 2 = 4 ( 10 0 2 ) x = 6 \& 5 =110_2 \& 101_2 = 4(100_2) x=6&5=1102&1012=4(1002)
y = 6 − 4 = 11 0 2 − 10 0 2 = 2 ( 01 0 2 ) y = 6-4=110_2-100_2 = 2(010_2) y=64=11021002=2(0102)

最后,使用对数换底公式处理一下y,就得到了jobs对应的下标。

对数换底公式:
l o g a b = l o g c b l o g c a log_ab = {log_c b \over log_c a} logab=logcalogcb

int nbit = pow(2,n);
//初始化 各个子集的和
vector<int> selectJobs(nbit,0);
for(int j=1;j<nbit;j++){
    int x = j & (j-1);
    int y = j - x;
    selectJobs[j] = selectJobs[x] + jobs[log(y)/log(2)];
}
快速枚举每种选取情况 j j j的子集 p p p

最简单的解法就是,再次枚举 [ 1 , j ] [1,j] [1,j],如果 p & j > 0 p\&j \gt 0 p&j>0 说明 p p p j j j的一个子集。

int nbit = pow(2,n);
for(int j=1;j<nbit;j++){
	for(int p=1;p<j;p++){
	    if(j&p){
		    // 如果 p 是 j 的一个子集
		    int sum = selectJobs[p];
		    int not_p_in_j = (~p) & j;
		    int maxTime = max(sum,dp[i-1][not_p_in_j]);
		    dp[i][j] = min(dp[i][j],maxTime);
		}
	}
}

但是这样时间复杂度为O( 2 n ⋅ 2 n 2^n\sdot 2^n 2n2n),太太慢了。有没有办法更快计算呢?


根据之前的 x & (x-1) 含义的启发(其实我也没有想明白 ),总之需要枚举 j j j的子集,就使用如下代码:

//p=0时结束
for(int p=j;p;p=(p-1)&j){
	//TO DO
}

代码

class Solution {
public:
    int minimumTimeRequired(vector<int>& jobs, int k) {
        int n = jobs.size();
        int nbit = pow(2,n);
        vector<vector<int>> dp(k,vector<int>(nbit,INT_MAX/2));

        //初始化 各个子集的和
        vector<int> selectJobs(nbit,0);
        for(int j=1;j<nbit;j++){
            int x = j & (j-1);
            int y = j - x;
            selectJobs[j] = selectJobs[x] + jobs[log(y)/log(2)];
        }

        //初始化dp[0][i]
        for(int i=0;i<nbit;i++){
            dp[0][i] = selectJobs[i];
        }

        //开始dp
        for(int i=1;i<k;i++){
        	//枚举选取情况
            for(int j=1;j<nbit;j++){
                //枚举j的子集
                for(int p=j;p;p=(p-1)&j){
                    int sum = selectJobs[p];
                    int not_p_in_j = (~p) & j;
                    // 或者 not_p_in_j = j - p;
                    int maxTime = max(sum,dp[i-1][not_p_in_j]);
                    dp[i][j] = min(dp[i][j],maxTime);
                }
            }
        }
        return dp[k-1][nbit-1];
    }
};

算法复杂度

时间复杂度: O( k ⋅ 3 n k \sdot 3^n k3n)。其中 n 是数组 jobs 的长度,k为工人个数。我们需要 O( 2 n 2^n 2n) 的时间预处理 selectJobs数组。内层循环复杂度只有O( 3 n 3^n 3n)而不是O( 2 n ⋅ 2 n 2^n \sdot 2^n 2n2n) = O( 4 n 4^n 4n)。
因此动态规划的时间复杂度为O( k ⋅ 3 n k\sdot 3^n k3n),故总时间复杂度为 O( k ⋅ 3 n k \sdot 3^n k3n)。
空间复杂度: O( k ⋅ 2 n k \sdot 2^n k2n)。其中 n 是数组 jobs 的长度,k为工人个数。用于存储dp数组。

在这里插入图片描述
官方答案dp能有500ms,不知道哪里慢了。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值