funcminimumTimeRequired(jobs []int, k int)int { n := len(jobs) sort.Sort(sort.Reverse(sort.IntSlice(jobs))) l, r := jobs[0], 0 for _, v := range jobs { r += v } return l + sort.Search(r-l, func(limit int)bool { limit += l workloads := make([]int, k) var backtrack func(int)bool backtrack = func(idx int)bool { if idx == n { returntrue } cur := jobs[idx] for i := range workloads { if workloads[i]+cur <= limit { workloads[i] += cur if backtrack(idx + 1) { returntrue } workloads[i] -= cur } // 如果当前工人未被分配工作,那么下一个工人也必然未被分配工作 // 或者当前工作恰能使该工人的工作量达到了上限 // 这两种情况下我们无需尝试继续分配工作 if workloads[i] == 0 || workloads[i]+cur == limit { break } } returnfalse } return backtrack(0) }) }
boolbacktrack(int* jobs, int jobsSize, int* workloads, int workloadsSize, int idx, int limit) { if (idx >= jobsSize) { returntrue; } int cur = jobs[idx]; for (int i = 0; i < workloadsSize; i++) { if (workloads[i] + cur <= limit) { workloads[i] += cur; if (backtrack(jobs, jobsSize, workloads, workloadsSize, idx + 1, limit)) { returntrue; } workloads[i] -= cur; } // 如果当前工人未被分配工作,那么下一个工人也必然未被分配工作 // 或者当前工作恰能使该工人的工作量达到了上限 // 这两种情况下我们无需尝试继续分配工作 if (workloads[i] == 0 || workloads[i] + cur == limit) { break; } } returnfalse; }
boolcheck(int* jobs, int jobsSize, int k, int limit) { int workloads[k]; memset(workloads, 0, sizeof(workloads)); return backtrack(jobs, jobsSize, workloads, k, 0, limit); }
intcmp(int* a, int* b) { return *b - *a; }
intaccumulate(int* arr, int* arrSize) { int ret = 0; for (int i = 0; i < arrSize; i++) { ret += arr[i]; } return ret; }
intminimumTimeRequired(int* jobs, int jobsSize, int k) { qsort(jobs, jobsSize, sizeof(int), cmp); int l = jobs[0], r = accumulate(jobs, jobsSize); while (l < r) { int mid = (l + r) >> 1; if (check(jobs, jobsSize, k, mid)) { r = mid; } else { l = mid + 1; } } return l; }
复杂度分析
时间复杂度:O(n \log n + \log (S-M) \times k^n),其中 n 是数组 jobs 的长度,S 是数组 jobs 的元素之和,M 是数组 jobs 中元素的最大值。最坏情况下每次二分需要遍历所有分配方案的排列,但经过一系列优化后,实际上可以规避掉绝大部分不必要的计算。
空间复杂度:O(n)。空间复杂度主要取决于递归的栈空间的消耗,而递归至多有 n 层。
方法二:动态规划 + 状态压缩
思路及算法
按照朴素的思路,我们按顺序给每一个工人安排工作,注意到当我们给第 i 个工人分配工作的时候,能够选择的分配方案仅和前 i-1 个人被分配的工作有关。因此我们考虑使用动态规划解决本题,只需要记录已经被分配了工作的工人数量,以及已经被分配的工作是哪些即可。
因为工作数量较少,我们可以使用状态压缩的方式来表示已经被分配的工作是哪些。具体地,假设有 n 个工作需要被分配,我们就使用一个 n 位的二进制整数来表示哪些工作已经被分配,哪些工作尚未被分配,如果该二进制整数的第 i 位为 1,那么第 i 个工作已经被分配,否则第 i 个工作尚未被分配。如有 3 个工作需要被分配,那么 5=(101)_2 即代表 第 0 和第 2 个工作已经被分配,第 1 个工作还未被分配。
这样我们可以写出状态方程:f[i][j] 表示给前 i 个人分配工作,工作的分配情况为 j 时,完成所有工作的最短时间。注意这里的 j 是一个二进制整数,表示了工作的分配情况。实际上我们也可以将 j 看作一个集合,包含了已经被分配的工作。
classSolution { public: intminimumTimeRequired(vector<int>& jobs, int k){ int n = jobs.size(); vector<int> sum(1 << n); for (int i = 1; i < (1 << n); i++) { int x = __builtin_ctz(i), y = i - (1 << x); sum[i] = sum[y] + jobs[x]; }
vector<vector<int>> dp(k, vector<int>(1 << n)); for (int i = 0; i < (1 << n); i++) { dp[0][i] = sum[i]; }
for (int i = 1; i < k; i++) { for (int j = 0; j < (1 << n); j++) { int minn = INT_MAX; for (int x = j; x; x = (x - 1) & j) { minn = min(minn, max(dp[i - 1][j - x], sum[x])); } dp[i][j] = minn; } } return dp[k - 1][(1 << n) - 1]; } };
publicclassSolution { publicintMinimumTimeRequired(int[] jobs, int k) { int n = jobs.Length; int[] sum = newint[1 << n]; for (uint i = 1; i < (1 << n); i++) { int x = NumberOfTrailingZeros(i), y = (int)i - (int)(1 << x); sum[i] = sum[y] + jobs[x]; }
int[,] dp = newint[k, 1 << n]; for (int i = 0; i < (1 << n); i++) { dp[0, i] = sum[i]; }
for (int i = 1; i < k; i++) { for (int j = 0; j < (1 << n); j++) { int minn = int.MaxValue; 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]; }
privatestaticintNumberOfTrailingZeros(uint i) { uint y; if (i == 0) return32; int n = 31; y = i <<16; if (y != 0) { n = n -16; i = y; } y = i << 8; if (y != 0) { n = n - 8; i = y; } y = i << 4; if (y != 0) { n = n - 4; i = y; } y = i << 2; if (y != 0) { n = n - 2; i = y; } return (int)n - (int)((i << 1) >> 31); } }
var minimumTimeRequired = function(jobs, k) { const n = jobs.length; const sum = newArray(1 << n).fill(0); for (let i = 1; i < (1 << n); i++) { const x = NumberOfTrailingZeros(i), y = i - (1 << x); sum[i] = sum[y] + jobs[x]; }
const dp = newArray(k).fill(0).map(() =>newArray(1 << n).fill(0)); for (let i = 0; i < (1 << n); i++) { dp[0][i] = sum[i]; }
for (let i = 1; i < k; i++) { for (let j = 0; j < (1 << n); j++) { let minn = Number.MAX_VALUE; for (let 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]; };
funcminimumTimeRequired(jobs []int, k int)int { n := len(jobs) m := 1 << n sum := make([]int, m) for i := 1; i < m; i++ { x := bits.TrailingZeros(uint(i)) y := i ^ 1<<x sum[i] = sum[y] + jobs[x] }
dp := make([][]int, k) for i := range dp { dp[i] = make([]int, m) } for i, s := range sum { dp[0][i] = s }
for i := 1; i < k; i++ { for j := 0; j < (1 << n); j++ { minn := math.MaxInt64 for x := j; x > 0; x = (x - 1) & j { minn = min(minn, max(dp[i-1][j-x], sum[x])) } dp[i][j] = minn } } return dp[k-1][(1<<n)-1] }
funcmin(a, b int)int { if a < b { return a } return b }
funcmax(a, b int)int { if a > b { return a } return b }
intminimumTimeRequired(int* jobs, int jobsSize, int k) { int n = jobsSize; int sum[1 << n]; sum[0] = 0; for (int i = 1; i < (1 << n); i++) { int x = __builtin_ctz(i), y = i - (1 << x); sum[i] = sum[y] + jobs[x]; } int dp[k][1 << n]; for (int i = 0; i < (1 << n); i++) { dp[0][i] = sum[i]; }
for (int i = 1; i < k; i++) { for (int j = 0; j < (1 << n); j++) { int minn = INT_MAX; for (int x = j; x; x = (x - 1) & j) { minn = fmin(minn, fmax(dp[i - 1][j - x], sum[x])); } dp[i][j] = minn; } } return dp[k - 1][(1 << n) - 1]; }
复杂度分析
时间复杂度:O(n\times 3^n),其中 n 是数组 jobs 的长度。我们需要 O(2^n) 的时间预处理 sum 数组。动态规划中共有 O(n \times 2^n) 种状态,将每个状态看作集合,大小为 k 的集合有 n \times C_n^k 个,其转移个数为 2^k,根据二项式定理有