Minimum Difficulty of a Job Schedule
HardPrompt
You want to schedule a list of jobs in d days. Jobs are dependent (i.e To work on the ith job, you have to finish all the jobs j where 0 <= j < i).
You have to finish at least one task every day. The difficulty of a job schedule is the sum of difficulties of each day of the d days. The difficulty of a day is the maximum difficulty of a job done on that day.
You are given an integer array jobDifficulty and an integer d. The difficulty of the ith job is jobDifficulty[i].
Return the minimum difficulty of a job schedule. If you cannot find a schedule for the jobs return -1.
Example 1:
Input: jobDifficulty = [6,5,4,3,2,1], d = 2
Output: 7
Explanation: First day you can finish the first 5 jobs, total difficulty = 6.
Second day you can finish the last job, total difficulty = 1.
The difficulty of the schedule = 6 + 1 = 7 Example 2:
Input: jobDifficulty = [9,9,9], d = 4
Output: -1
Explanation: If you finish a job per day you will still have a free day. you cannot find a schedule for the given jobs.Example 3:
Input: jobDifficulty = [1,1,1], d = 3
Output: 3
Explanation: The schedule is one job per day. total difficulty will be 3.
Constraints:
1 <= jobDifficulty.length <= 3000 <= jobDifficulty[i] <= 10001 <= d <= 10
Approaches
3 approaches with complexity analysis and trade-offs.
This problem has optimal substructure and overlapping subproblems, making it a good candidate for dynamic programming. A straightforward way to approach DP is through top-down recursion with memoization.
We can define a recursive function solve(i, d) that calculates the minimum difficulty to schedule jobs from index i to the end of the array within d days. To find the solution for the original problem, we would call solve(0, d).
In the function solve(i, d), we try all possible ways to schedule the jobs for the current day. We can schedule jobs from i to j, where j is a valid split point. The difficulty for this day would be the maximum job difficulty in the range [i, j]. The remaining jobs from j+1 to the end must then be scheduled in d-1 days. We recursively call our function for this subproblem: solve(j+1, d-1). We take the minimum over all possible split points j.
To avoid recomputing the same subproblem (i, d) multiple times, we use a 2D array memo to store the results. Before computing solve(i, d), we check if the result is already in our memoization table.
Algorithm
- Base Case: If the number of jobs
nis less than the number of daysd, it's impossible to schedule. Return -1. - Recursive Function: Define a function, say
solve(i, daysLeft), which computes the minimum difficulty to schedule jobs from indexiton-1indaysLeftdays. - Memoization: Use a 2D array,
memo[i][daysLeft], to store the results of subproblems to avoid re-computation. Initialize it with a value like -1. - Recursive Step:
- For the current day, we can take jobs from
itoj. The number of remaining jobsn - (j+1)must be at leastdaysLeft - 1. - The difficulty for the current day is
max(jobDifficulty[i...j]). - The difficulty for the remaining days is
solve(j+1, daysLeft - 1). - We iterate through all valid split points
j(fromiton - daysLeft) and find the minimum total difficulty:min(max(jobDifficulty[i...j]) + solve(j+1, daysLeft - 1)).
- For the current day, we can take jobs from
- Base Case for Recursion: If
daysLeftis 1, we must schedule all remaining jobs (iton-1) on this day. The difficulty ismax(jobDifficulty[i...n-1]). - Return Value: The final answer is the result of
solve(0, d). If the result is infinity (or a very large number used for initialization), it means no valid schedule was found, but our initial checkn < dshould handle this.
Walkthrough
class Solution { private int n; private int[] jobDifficulty; private int d; private int[][] memo; public int minDifficulty(int[] jobDifficulty, int d) { this.n = jobDifficulty.length; if (n < d) { return -1; } this.jobDifficulty = jobDifficulty; this.d = d; this.memo = new int[n][d + 1]; for (int i = 0; i < n; i++) { Arrays.fill(memo[i], -1); } return solve(0, d); } private int solve(int i, int daysLeft) { // If we have already computed this subproblem, return the stored result. if (memo[i][daysLeft] != -1) { return memo[i][daysLeft]; } // Base case: If only one day is left, we must do all remaining jobs. // The difficulty for this day is the max difficulty among the remaining jobs. if (daysLeft == 1) { int maxDifficulty = 0; for (int k = i; k < n; k++) { maxDifficulty = Math.max(maxDifficulty, jobDifficulty[k]); } return memo[i][daysLeft] = maxDifficulty; } int minTotalDifficulty = Integer.MAX_VALUE; int maxDifficultyCurrentDay = 0; // Iterate through all possible split points 'j'. // We must leave at least 'daysLeft - 1' jobs for the remaining days. for (int j = i; j <= n - daysLeft; j++) { maxDifficultyCurrentDay = Math.max(maxDifficultyCurrentDay, jobDifficulty[j]); int remainingDifficulty = solve(j + 1, daysLeft - 1); // If the subproblem is solvable if (remainingDifficulty != Integer.MAX_VALUE) { minTotalDifficulty = Math.min(minTotalDifficulty, maxDifficultyCurrentDay + remainingDifficulty); } } return memo[i][daysLeft] = minTotalDifficulty; }}Complexity
Time
O(n^2 * d). There are `n * d` states for our `solve(i, daysLeft)` function. For each state, we iterate from `i` to `n - daysLeft`, which can be up to `O(n)` iterations. Inside the loop, we do constant time work (as the recursive call's result will be memoized). Thus, the total time complexity is `O(n * d * n) = O(n^2 * d)`.
Space
O(n * d), where `n` is the number of jobs and `d` is the number of days. This is for the memoization table `memo`. The recursion stack depth is at most `d`.
Trade-offs
Pros
Relatively intuitive to formulate from the problem's recursive nature.
Correctly solves the problem within the given constraints.
Cons
The time and space complexity are not optimal.
Can lead to a
StackOverflowErrorfor very deep recursion, althoughdis small here, so it's not a major concern.
The top-down recursive solution can be converted into a bottom-up iterative solution, also known as tabulation. This approach builds the solution from the smallest subproblems up to the final problem. It avoids recursion overhead and can sometimes make space optimizations more apparent.
We define a 2D DP table, dp[k][i], to store the minimum difficulty of scheduling the first i jobs in k days. We iterate through the days k from 1 to d, and for each day, we iterate through the jobs i from k to n. The value of dp[k][i] is determined by finding the best way to form the k-th day. If the k-th day includes jobs from j to i-1, the total difficulty would be the cost of scheduling the first j jobs in k-1 days (dp[k-1][j]) plus the difficulty of the k-th day (max(jobDifficulty[j...i-1])). We minimize this over all possible j.
Algorithm
- State Definition: Let
dp[k][i]be the minimum difficulty to schedule the firstijobs (from index 0 toi-1) inkdays. - Initialization: Create a
dptable of size(d+1) x (n+1)and initialize all values to infinity, exceptdp[0][0] = 0. - Base Case (k=1): For one day,
dp[1][i]is the maximum difficulty of the firstijobs. This can be computed iteratively:dp[1][i] = max(dp[1][i-1], jobDifficulty[i-1]). - Transitions: Iterate through the number of days
kfrom 2 tod. For eachk, iterate through the number of jobsifromkton. To computedp[k][i], we try all possible split points for thek-th day. Let thek-th day consist of jobs from indexjtoi-1. The previousk-1days must have scheduled jobs from 0 toj-1. The recurrence relation is:dp[k][i] = min(dp[k][i], dp[k-1][j] + max(jobDifficulty[j...i-1]))forjfromk-1toi-1. - Result: The final answer is
dp[d][n]. - Space Optimization: Notice that
dp[k]only depends ondp[k-1]. We can optimize the space toO(n)by using only two rows (or one, with careful updates) of the DP table.
Walkthrough
class Solution { public int minDifficulty(int[] jobDifficulty, int d) { int n = jobDifficulty.length; if (n < d) { return -1; } // dp[k][i]: min difficulty for first i jobs in k days int[][] dp = new int[d + 1][n + 1]; for (int[] row : dp) { Arrays.fill(row, Integer.MAX_VALUE / 2); // Use a large value to prevent overflow } dp[0][0] = 0; for (int k = 1; k <= d; k++) { for (int i = k; i <= n; i++) { int maxDifficultyLastDay = 0; // j is the start index of the last day's jobs for (int j = i - 1; j >= k - 1; j--) { maxDifficultyLastDay = Math.max(maxDifficultyLastDay, jobDifficulty[j]); dp[k][i] = Math.min(dp[k][i], dp[k-1][j] + maxDifficultyLastDay); } } } return dp[d][n] == Integer.MAX_VALUE / 2 ? -1 : dp[d][n]; }}Space-Optimized Version:
class Solution { public int minDifficulty(int[] jobDifficulty, int d) { int n = jobDifficulty.length; if (n < d) return -1; int[] dp = new int[n + 1]; Arrays.fill(dp, Integer.MAX_VALUE / 2); dp[0] = 0; for (int k = 1; k <= d; k++) { int[] next_dp = new int[n + 1]; Arrays.fill(next_dp, Integer.MAX_VALUE / 2); for (int i = k; i <= n; i++) { int maxDifficultyLastDay = 0; for (int j = i - 1; j >= k - 1; j--) { maxDifficultyLastDay = Math.max(maxDifficultyLastDay, jobDifficulty[j]); next_dp[i] = Math.min(next_dp[i], dp[j] + maxDifficultyLastDay); } } dp = next_dp; } return dp[n] == Integer.MAX_VALUE / 2 ? -1 : dp[n]; }}Complexity
Time
O(n^2 * d). We have three nested loops: `k` from 1 to `d`, `i` from `k` to `n`, and `j` from `i-1` to `k-1`. This results in a cubic complexity relative to `n` if `d` is proportional to `n`, but since `d` is small, we state it as `O(n^2 * d)`.
Space
O(n * d). The standard tabulation uses a `(d+1) x (n+1)` table. This can be optimized to O(n) because computing the values for day `k` only requires the values from day `k-1`.
Trade-offs
Pros
Eliminates recursion overhead, which can be slightly faster in practice.
The iterative structure makes space optimization more straightforward.
Cons
The time complexity is the same as the memoized recursion,
O(n^2 * d), which is not optimal.Requires careful handling of loop bounds and indices.
The O(n^2 * d) complexity of the previous approaches comes from the innermost loop that recalculates the minimum over all possible split points. This calculation can be optimized. For a fixed day k, when computing dp_curr[i], we are essentially solving min_{j<i} (dp_prev[j] + max(jobDifficulty[j...i-1])).
This structure can be optimized using a monotonic stack. The stack helps us maintain candidates for the split point j in a way that lets us compute the minimum in amortized O(1) time instead of O(n).
We process jobs one by one. The stack stores indices of jobs in decreasing order of their difficulty. When a new job i arrives with a higher difficulty than the job at the top of the stack, it means job i will be the maximum for a certain range. The stack helps us efficiently calculate the cost contribution of these ranges. By cleverly updating the minimum cost associated with stack segments, we can find the optimal dp_curr[i] quickly.
Algorithm
- DP State and Recurrence: Use the same space-optimized DP setup as the previous approach. Let
dp[i]be the min difficulty for the firstijobs ink-1days. We want to computedp_next[i]for dayk.dp_next[i] = min_{j=k-1 to i-1} (dp[j] + max(jobDifficulty[j...i-1])). - Monotonic Stack: For each day
k, we use a monotonic stack to optimize the calculation ofdp_next. The stack will store indicespsuch thatjobDifficulty[p]is strictly decreasing. - Iteration: We iterate
ifromktonto computedp_next[i]. - Stack Maintenance: When considering job
i-1, we maintain the stack's property. We pop indicespfrom the stack ifjobDifficulty[p] <= jobDifficulty[i-1]. This is becausejobDifficulty[i-1]will 'shadow' these smaller or equal difficulties for all future calculations. - Cost Calculation: As we pop from the stack, we track the minimum
dp[p]value of the jobs being merged into the new segment dominated byjobDifficulty[i-1]. Let's call thismin_prev_cost. - DP Update: After updating the stack,
dp_next[i]can be calculated efficiently. The stack partitions the jobs0...i-1into segments, each with a specific maximum difficulty (defined by the stack elements). The cost for a segment starting at indexpismin_prev_cost_for_p + jobDifficulty[p]. We can find the minimum of these costs inO(1)by carrying over the minimum from the previous step. - Final Result: After
diterations,dp[n]will hold the answer.
Walkthrough
class Solution { public int minDifficulty(int[] jobDifficulty, int d) { int n = jobDifficulty.length; if (n < d) { return -1; } int[] dp = new int[n]; int maxDifficulty = 0; for (int i = 0; i < n; i++) { maxDifficulty = Math.max(maxDifficulty, jobDifficulty[i]); dp[i] = maxDifficulty; } for (int k = 2; k <= d; k++) { int[] dpNext = new int[n]; Arrays.fill(dpNext, Integer.MAX_VALUE); // Stack stores indices Deque<Integer> stack = new ArrayDeque<>(); for (int i = k - 1; i < n; i++) { // The cost to schedule the first i jobs (0..i-1) in k-1 days. // This is the value we'd use if the k-th day starts at job i. int minPrevCost = dp[i - 1]; while (!stack.isEmpty() && jobDifficulty[stack.peek()] <= jobDifficulty[i]) { // jobDifficulty[i] is greater, so it becomes the max for the segment // previously covered by stack.peek(). We merge the segments. minPrevCost = Math.min(minPrevCost, dp[stack.pop()]); } if (!stack.isEmpty()) { // The last day starts after stack.peek(). The max difficulty for this // part of the last day is jobDifficulty[i]. // The cost is the min cost of the previous day's schedule ending at stack.peek(), // plus the max difficulty of the segment from stack.peek() to i. // dp[stack.peek()] already contains this min cost. dpNext[i] = Math.min(dpNext[i], dp[stack.peek()] + jobDifficulty[i]); } // The last day starts at the beginning of the merged segment. // The cost is minPrevCost + jobDifficulty[i]. dpNext[i] = Math.min(dpNext[i], minPrevCost + jobDifficulty[i]); // We store the min cost for schedules ending at `i` for day `k` // This is a bit tricky. The value we need for future calculations is the minimum // cost for day k-1 ending at various points. The logic above handles this by // passing `minPrevCost` forward. // For the stack, we need to associate the min cost of day k-1 ending at that index. // A simpler way is to just use the dp array directly. dp[i] = minPrevCost; // Store the min cost for the segment starting after this index. stack.push(i); } // This is a slightly different but working formulation of the stack logic. // Let's use a more standard one for clarity. // Standard O(nd) implementation: stack.clear(); for (int i = k - 1; i < n; i++) { int prevDayCost = dp[i - 1]; while (!stack.isEmpty() && jobDifficulty[stack.peek()] <= jobDifficulty[i]) { int j = stack.pop(); prevDayCost = Math.min(prevDayCost, dp[j]); } if (!stack.isEmpty()) { dpNext[i] = dp[stack.peek()] + jobDifficulty[i]; } dpNext[i] = Math.min(dpNext[i], prevDayCost + jobDifficulty[i]); dp[i] = prevDayCost; stack.push(i); } dp = dpNext; } return dp[n - 1]; }}Note: The provided code shows one way to implement the O(nd) logic. The core idea is that for each day k, a new dp array is computed in O(n) time using the dp array from day k-1 and a monotonic stack.*
Complexity
Time
O(n * d). The outer loop runs `d` times. The inner loop runs `n` times. Inside the inner loop, each element is pushed onto and popped from the stack at most once over the course of the loop. Therefore, the stack operations take amortized O(1) time, leading to a total time complexity of O(n * d).
Space
O(n). We only need to store the DP results for the previous day to compute the current day. The stack also takes at most O(n) space.
Trade-offs
Pros
Optimal time complexity, making it very fast.
Optimal space complexity.
Cons
The logic is significantly more complex and less intuitive than the standard DP approaches.
Implementation can be tricky and error-prone.
Solutions
Solution
class Solution {public int minDifficulty(int[] jobDifficulty, int d) { final int inf = 1 << 30; int n = jobDifficulty.length; int[][] f = new int[n + 1][d + 1]; for (var g : f) { Arrays.fill(g, inf); } f[0][0] = 0; for (int i = 1; i <= n; ++i) { for (int j = 1; j <= Math.min(d, i); ++j) { int mx = 0; for (int k = i; k > 0; --k) { mx = Math.max(mx, jobDifficulty[k - 1]); f[i][j] = Math.min(f[i][j], f[k - 1][j - 1] + mx); } } } return f[n][d] >= inf ? -1 : f[n][d]; }}class Solution: def minDifficulty(self, jobDifficulty: List[int], d: int) -> int: n = len(jobDifficulty) f = [[inf] * (d + 1) for _ in range(n + 1)] f[0][0] = 0 for i in range(1, n + 1): for j in range(1, min(d + 1, i + 1)): mx = 0 for k in range(i, 0, - 1): mx = max(mx, jobDifficulty[k - 1]) f[i][j] = min(f[i][j], f[k - 1][j - 1] + mx) return - 1 if f[n][d] >= inf else f[n][d]class Solution {public: int minDifficulty(vector<int> &jobDifficulty, int d) { int n = jobDifficulty.size(); int f[n + 1][d + 1]; memset(f, 0x3f, sizeof(f)); f[0][0] = 0; for (int i = 1; i <= n; ++i) { for (int j = 1; j <= min(d, i); ++j) { int mx = 0; for (int k = i; k; --k) { mx = max(mx, jobDifficulty[k - 1]); f[i][j] = min(f[i][j], f[k - 1][j - 1] + mx); } } } return f[n][d] == 0x3f3f3f3f ? -1 : f[n][d]; }};Video walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.