今天的每日一题比较难,看了很久没有什么思路,所以看了官方题解,好在看懂第一个方法了,下面放上代码和自己的理解。下午考完试再来研究dp的解法和其他大佬提出的解法。
思路
根据题目要求,是要在完成全部工作的前提下,找到工人最大工作时间的最小值,所以一个最先想到的方法就是不断枚举和尝试,把不同的工作分给不同的工人,然后在可以完成全部工作的方案中,找到每个方案最大值中的最小值(也就是把每个可以完成全部工作的方案中,工人最大工作时长拿出来进行比较,找到这些最大值中的最小值),此时,我们的目标变成了两个:
- 找到所有可以完成全部工作的分配方案;
- 找到方案中最大值中的最小值limit;
在目标1中,为了减少不必要的计算,我们可以做一些“剪枝”。
- 因为每个工作都要分配到工人,所以limit的理论最小值为工作中时长最大的那一个;考虑到极端情况,所有的工作让一个人做,所以limit的理论最大值为所有工作时长的总和,因此limit的范围限制到了 [max(jobs), sum(jobs)] ;
- 因为是要找到所有满足要求的分配方案,所以要对所有可能的limit进行遍历,当 ( i + 1 ) ∈ [ 1 , k ] (i+1) \in [1,k] (i+1)∈[1,k] 时,如果工人i当前的工作时长超过limit,则后续的工人就不用再进行分配了,这样又减少了一部分的计算;
- 如何才是满足条件的分配方案呢?也就是在当前limit下,所有的工人都已经分配了工作,且每个人的工作时长均不大于limit,则证明当前的limit是合理的;
- 因为只要limit=n时满足要求,那么对于任意的limit>n,均满足条件,所以可以使用二分查找寻找最小的limit值,因此又减少了部分计算开销。
综上,可以得到如下的代码:
代码
class Solution {
public int minimumTimeRequired(int[] jobs, int k) {
//因为先分配工作时间长的工作会方便后续的分配,为了方便操作,对数组进行逆序
Arrays.sort(jobs);//默认的到的是升序
int left=0,right=jobs.length-1;
while(left<right)//如果不进行降序排序,也能得到准确的结果,也能ac,但是时间开销比较大
{
int temp = jobs[right];
jobs[right] = jobs[left];
jobs[left] = temp;
left++;
right--;
}
//为了减少不必要的计算,设置上限为所有工作之和,下限为时长最大的工作
int l = jobs[0], r = Arrays.stream(jobs).sum();
while(l<r)
{
int mid = (l+r)>>1;//因为使用二分查找,所以这里获取中间值
if(check(jobs, mid, k))//如果把mid作为limit可以成立,那么对于limit>mid均成立
r = mid;
else
l = mid + 1;
}
return l;
}
public boolean check(int[] jobs, int limit, int k)
{
int[] workArrange = new int[k];//设置一个长度为工人数量的数组,用于存储每个工人对应的工作时长
return backTrack(jobs, workArrange, 0 , limit);
}
public boolean backTrack(int[] jobs, int[] workArrange, int i, int limit)
{
if (i>=jobs.length)//满足条件的方案
return true;
int curWork = jobs[i];
for(int j=0;j<workArrange.length;j++)
{
if(workArrange[j]+curWork <= limit)//对每个工人进行工作分配,如果满足条件则分配,不满足的话,后续的工人也不用分配工作了
{
workArrange[j] += curWork;
if (backTrack(jobs,workArrange,i+1,limit))//如果已经是最后一个工人了,那么证明该方案满足条件
return true;
workArrange[j] -= curWork;//经过上面的递归,发现把第i个工作分配给第j个人不满足条件,那么就不能分配给工人j工作i,所以删除
}
if (workArrange[j] == 0 || workArrange[j]+curWork == limit)
break;
}
return false;
}
}
------------------------分割线------------------------------------
来更新状态压缩的dp解法了,是在有些难理解。。。如果我的理解存在错误,请指正,先谢谢各位了!
思路
本方法的重点部分在于
for(int i=1;i<(1<<n);i++)
{
int x = Integer.numberOfTrailingZeros(i);
int y = i-(1<<x);
sum[i] = sum[y] + jobs[x];
}
具体的解释详见代码内的注释,结合输入案例的结果更加容易理解。
代码
class Solution {
public int minimumTimeRequired(int[] jobs, int k)
{
int n = jobs.length;
//使用状态压缩方法,因为对于n个工作而言,每个工作的状态只有两种——以被分配1 和 未被分配0,所以可以使用状态压缩辅助计算
int[] sum = new int[1<<n];//因为状态压缩就是使用二进制数辅助计算,为了方便对每个工作的状态进行操作,这里设置了一个2的n次方的数组
for(int i=1;i<(1<<n);i++)
{
//返回i的二进制形式中最后一个1之后0的数量,如果i为0,则返回32
int x = Integer.numberOfTrailingZeros(i);//x得到的是当前的分配方案于上一个分配方案之间所缺少的工作的数组索引值,如当前的方案为i=10--"1010",与其上一个方案y=8--"1000"相比,第二个工作被分配了,第二个工作对应的索引是1,所以此时的x=1
//输入为{1,2,4,7,8}, 2 时,x的值0 1 0 2 0 1 0 3 0 1 0 2 0 1 0 4 0 1 0 2 0 1 0 3 0 1 0 2 0 1 0
int y = i-(1<<x);//y得到的是上一个分配方案,如i=2时,y=0;原因是2的二进制为"10",由"00"改变得到的,为什么不是1呢,因为1对于的二进制是"1",只有一位,而2对应的"10"是两位,其余的依此类推
//输入为{1,2,4,7,8}, 2 时,y的值0 0 2 0 4 4 6 0 8 8 10 8 12 12 14 0 16 16 18 16 20 20 22 16 24 24 26 24 28 28 30
//sum数组存储的是不同分配方案下,对应的工作时长,方便后面直接取用
sum[i] = sum[y] + jobs[x];//输入为{1,2,4,7,8}, 2 时,sum数组中的值 0 1 2 3 4 5 6 7 7 8 9 10 11 12 13 14 8 9 10 11 12 13 14 15 15 16 17 18 19 20 21 22
}
//因为当对第i个工人分配工作时,分配方案由前i-1个工人的分配方案所决定或者说是限制,所以使用dp算法
int [][] dp = new int[k][1<<n];//对于dp[i][j],i表示第几个工人,j表示目前分配的方案(因为使用的是状态压缩,所以j的二进制形式表示不同的工作分配方式)
for(int i=0; i<(1<<n); i++)//使用sum数组对dp[0]进行初始化
dp[0][i] = sum[i];
for(int i=1; i<k; i++)//对于k个工人,逐个分配工作
for(int j=0; j<(1<<n);j++)//对于(1<<n)种分配方案,逐个尝试
{
int min = Integer.MAX_VALUE;
for (int x = j; x != 0; x = (x-1)&j)//对前j个方案依次进行测试,得到每种分配方案的工作时长最小值
min = Math.min(min, Math.max(dp[i-1][j-x], sum[x]));
dp[i][j] = min;
}
return dp[k-1][(1<<n)-1];//dp的最后一个值意味着所有的工人已经分配完了全部的工作的方案中的最小值,所以为最终的答案
}
测试用例–便于理解
当输入为[1,2,4,7,8],k=2时,对应的x和y的值,方便理解
i | x | y |
---|---|---|
1 | 0 | 0 |
2 | 1 | 0 |
3 | 0 | 2 |
4 | 2 | 0 |
5 | 0 | 4 |
6 | 1 | 4 |
7 | 0 | 6 |
8 | 3 | 0 |
9 | 0 | 8 |
10 | 1 | 8 |
11 | 0 | 10 |
12 | 2 | 8 |
13 | 0 | 12 |
14 | 1 | 12 |
15 | 0 | 14 |
16 | 4 | 0 |
17 | 0 | 16 |
18 | 1 | 16 |
19 | 0 | 18 |
20 | 2 | 16 |
21 | 0 | 20 |
22 | 1 | 20 |
23 | 0 | 22 |
24 | 3 | 16 |
25 | 0 | 24 |
26 | 1 | 24 |
27 | 0 | 26 |
28 | 2 | 24 |
29 | 0 | 28 |
30 | 1 | 28 |
31 | 0 | 30 |