这题是一月份某次周赛的最后一题,记得当时提交了好多次都没过。题目本质就是将n个数分成k份,找出和最大的那份。然后问怎么分使得和最大的那份在所有分法中最小。
解法一:动态规划+状态压缩。
令dp[i][j]表示前[0,i]个工人完成任务集j的最小最大工作时间。任务集j就是j的二进制数中bit为1的那些任务。举个例子,有3个任务,则共有种任务集,对于任务集5,5的二进制表示为101,则表示选择了任务1和任务3,即任务集5表示{任务1,任务3}.该题中任务数最多为12,因此最多有种任务集。要计算dp[i][j],假设第i个工人完成工作集t,则dp[i][j]=max(dp[i-1][j-t],total[t]),我们选取令dp[i][j]最小的t即可.时间复杂度。
var minimumTimeRequired = function(jobs, k) {
let n = jobs.length
const inf = 9999999999
let dp = new Array(k).fill(null).map(d=>new Array(1<<n).fill(inf))
// 预处理一下,计算任务集j所对应的所有任务需要完成的时间
let total = [0]
for (let j = 1; j < (1<<n); j++) {
for (let t = 0; t < n; t++) {
if ((j&(1<<t))!=0) {
total[j] = total[j-(1<<t)] + jobs[t]
break
}
}
}
for (let j = 0; j < (1<<n); j++) dp[0][j] = total[j]
for (let i = 1; i < k; i++) {
for (let j = 0; j < (1<<n); j++) {
// 要计算dp[i][j],假设第i个工人完成工作集t,则dp[i][j]=max(dp[i-1][j-t],total[t]),我们选取令dp[i][j]最小的t即可
for (let t = 0; t <= j; t++) {
// 判断t是不是j的子集
if ((t|j)==j) {
dp[i][j] = Math.min(dp[i][j], Math.max(dp[i-1][j-t], total[t]))
}
}
}
}
return dp[k-1][(1<<n)-1]
};
参考官方答案,上面对于t的遍历还能优化成如下写法。其中(t-1)&j就是找到小于t的j的最大子集。
for (let t = j; t > 0; t = (t-1)&j) {
dp[i][j] = Math.min(dp[i][j], Math.max(dp[i-1][j-t], total[t]))
}
解法二:二分+回溯+剪枝
上面解法是在我对题目有印象时想到的,如果是第一次遇到这题很难会想到上面解法。对于这种找最值的题,我们往往可以考虑使用二分法,即对于一个值limit,我们去检查当限定m时,是否存在这样一种分法使得最大的那个不超过limit。所以现在问题转变为check if valid了。我们使用回溯来check是否有效,定义一个数组workloads,workloads[i]表示第i个工人分配到的工作量,然后进行遍历+回溯。同时我们加上剪枝来进行优化。参考力扣官方答案。
var minimumTimeRequired = function(jobs, k) {
let left = Math.max(...jobs), right = jobs.reduce((a,b)=>a+b)
while (left<right) {
const limit = (left+right)>>1
if (check(limit)) right = limit
else left = limit+1
}
return left
function check(limit) {
let workloads = new Array(k).fill(0)
return dfs(0)
function dfs(idx) {
if (idx >= jobs.length) return true
for (let i = 0; i < k; i++) {
if (workloads[i]+jobs[idx]<=limit) {
workloads[i] += jobs[idx]
if (dfs(idx+1)) return true
workloads[i] -= jobs[idx]
}
if (workloads[i] == 0) break
}
return false
}
}
};