1986. Minimum Number of Work Sessions to Finish the Tasks
Problem Description
You are given n
tasks, where each task has a specific duration. The task durations are provided in an array tasks
of length n
, where tasks[i]
represents the number of hours needed to complete the i
-th task.
You need to complete all tasks using work sessions. A work session is a continuous period where you work for at most sessionTime
hours before taking a break. The following rules must be followed:
- Once you start a task in a work session, you must finish it in the same session - you cannot split a task across multiple sessions
- You can start a new task immediately after finishing the previous one - there's no gap needed between tasks within the same session
- You can complete the tasks in any order - you're free to rearrange the sequence of tasks
Your goal is to find the minimum number of work sessions needed to complete all the tasks while following these constraints.
The problem guarantees that sessionTime
is greater than or equal to the largest task duration, ensuring that every individual task can be completed within a single session.
For example, if you have tasks [1, 2, 3]
and sessionTime = 3
:
- One possible arrangement: Session 1 completes task with duration 3, Session 2 completes tasks with durations 1 and 2 (total 3 hours). This requires 2 sessions.
- Another arrangement: Session 1 completes tasks with durations 1 and 2, Session 2 completes task with duration 3. This also requires 2 sessions.
The problem asks you to return the minimum number of such work sessions needed.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- No: The problem involves tasks and work sessions, not nodes and edges in a graph structure.
Need to solve for kth smallest/largest?
- No: We're looking for the minimum number of work sessions, not the kth smallest or largest element.
Involves Linked Lists?
- No: The problem uses an array of tasks, not linked lists.
Does the problem have small constraints?
- Yes: The problem states that
n
(number of tasks) does not exceed 14, which is a small constraint. This is a strong indicator that exponential-time solutions might be acceptable.
Brute force / Backtracking?
- Yes: With small constraints (n ≤ 14), we can explore different combinations of tasks in work sessions. We need to try different ways to group tasks into sessions, making backtracking a suitable approach.
Conclusion: The flowchart suggests using a backtracking approach for this problem. The small constraint (n ≤ 14) allows us to explore all possible ways to assign tasks to work sessions. We can recursively try placing each task in existing sessions or creating new sessions, backtracking when a configuration doesn't work, and keeping track of the minimum number of sessions needed.
The backtracking approach would:
- Try assigning each task to an existing work session if it fits
- If no existing session can accommodate the task, create a new session
- Recursively solve for the remaining tasks
- Backtrack and try different assignments to find the minimum number of sessions
Intuition
When we see that n ≤ 14
, this is a strong hint that we can use exponential-time algorithms. With 14 tasks, we have 2^14 = 16,384
possible states, which is computationally manageable.
The key insight is that we need to find the optimal way to group tasks into sessions. Each valid grouping of tasks that fits within sessionTime
can form one work session. The challenge is finding the minimum number of such groupings that cover all tasks.
Let's think about this problem differently: imagine we have a set of tasks, and we want to know the minimum sessions needed. We can break this down by considering:
- What combinations of tasks can fit in a single session?
- How can we combine these valid sessions to cover all tasks with minimum sessions?
This naturally leads us to think about state compression - we can represent any subset of tasks as a binary number. For example, with tasks [1, 2, 3]
, the binary 101
means we've selected the 1st and 3rd tasks.
The first step is to identify all valid sessions. We can precompute which subsets of tasks can fit in a single session by checking if their total time is at most sessionTime
. This gives us all possible "building blocks" for our solution.
The second step uses dynamic programming. For any state i
(representing a set of completed tasks), we ask: "What's the minimum number of sessions needed to complete these tasks?" We can answer this by trying all valid sessions j
that are subsets of i
. If we complete session j
, we're left with tasks i ^ j
(tasks in i
but not in j
). So:
f[i] = min(f[i ^ j] + 1)
for all valid sessions j ⊆ i
Starting from f[0] = 0
(no sessions needed for no tasks), we build up to f[2^n - 1]
(all tasks completed), which gives us our answer.
The beauty of this approach is that it systematically explores all possible ways to partition tasks into sessions while avoiding redundant calculations through memoization.
Learn more about Dynamic Programming, Backtracking and Bitmask patterns.
Solution Approach
The solution implements the state compression dynamic programming approach with subset enumeration. Let's walk through the implementation step by step:
Step 1: Precompute Valid Sessions
First, we identify which subsets of tasks can be completed in a single session:
ok = [False] * (1 << n)
for i in range(1, 1 << n):
t = sum(tasks[j] for j in range(n) if i >> j & 1)
ok[i] = t <= sessionTime
- We create a boolean array
ok
of size2^n
whereok[i]
indicates whether the subset represented by binary numberi
can fit in one session - For each subset
i
, we calculate the total time by checking each bit: if bitj
is set (i >> j & 1
), we includetasks[j]
in the sum - If the total time is at most
sessionTime
, this subset forms a valid session
Step 2: Dynamic Programming with Subset Enumeration
Next, we compute the minimum sessions needed for each state:
f = [inf] * (1 << n)
f[0] = 0
for i in range(1, 1 << n):
j = i
while j:
if ok[j]:
f[i] = min(f[i], f[i ^ j] + 1)
j = (j - 1) & i
- Initialize
f[0] = 0
(base case: 0 sessions for 0 tasks) and all other states to infinity - For each state
i
, we enumerate all its subsetsj
- The subset enumeration uses the technique
j = (j - 1) & i
:- Starting with
j = i
, this generates all subsets ofi
in descending order - The operation
(j - 1) & i
moves to the next smaller subset - When
j
becomes 0, we stop
- Starting with
- For each valid session
j
(whereok[j]
is true), we try using it:i ^ j
represents the remaining tasks after completing sessionj
f[i ^ j] + 1
is the total sessions: sessions for remaining tasks plus the current session- We keep the minimum across all possibilities
Step 3: Return the Result
return f[-1]
The answer is f[2^n - 1]
or f[-1]
, which represents the minimum sessions needed when all n
tasks are completed (all bits set to 1).
Time Complexity: O(3^n)
- for each of the 2^n
states, we enumerate all its subsets, which takes O(2^k)
time where k
is the number of 1-bits. The sum over all states gives us 3^n
.
Space Complexity: O(2^n)
- for storing the ok
and f
arrays.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a concrete example with tasks = [2, 3, 2]
and sessionTime = 4
.
Step 1: Identify Valid Sessions
We need to check which combinations of tasks can fit in a single 4-hour session. Using binary representation where bit positions 0, 1, 2 correspond to tasks of duration 2, 3, 2:
001
(task 0 only): duration = 2 ≤ 4 ✓010
(task 1 only): duration = 3 ≤ 4 ✓011
(tasks 0,1): duration = 2+3 = 5 > 4 ✗100
(task 2 only): duration = 2 ≤ 4 ✓101
(tasks 0,2): duration = 2+2 = 4 ≤ 4 ✓110
(tasks 1,2): duration = 3+2 = 5 > 4 ✗111
(all tasks): duration = 2+3+2 = 7 > 4 ✗
Valid sessions: {001, 010, 100, 101}
Step 2: Dynamic Programming
Initialize f[0] = 0
(no sessions for no tasks), all others to infinity.
Process each state and enumerate its subsets:
-
State
001
(task 0):- Subset
001
: valid session,f[001] = f[000] + 1 = 0 + 1 = 1
- Subset
-
State
010
(task 1):- Subset
010
: valid session,f[010] = f[000] + 1 = 0 + 1 = 1
- Subset
-
State
011
(tasks 0,1):- Subset
001
: valid,f[011] = f[010] + 1 = 1 + 1 = 2
- Subset
010
: valid,f[011] = min(2, f[001] + 1) = min(2, 2) = 2
- Subset
-
State
100
(task 2):- Subset
100
: valid session,f[100] = f[000] + 1 = 0 + 1 = 1
- Subset
-
State
101
(tasks 0,2):- Subset
101
: valid session,f[101] = f[000] + 1 = 0 + 1 = 1
✓ - Subset
100
: valid,f[101] = min(1, f[001] + 1) = min(1, 2) = 1
- Subset
001
: valid,f[101] = min(1, f[100] + 1) = min(1, 2) = 1
- Subset
-
State
110
(tasks 1,2):- Subset
100
: valid,f[110] = f[010] + 1 = 1 + 1 = 2
- Subset
010
: valid,f[110] = min(2, f[100] + 1) = min(2, 2) = 2
- Subset
-
State
111
(all tasks):- Subset
101
: valid,f[111] = f[010] + 1 = 1 + 1 = 2
✓ - Subset
100
: valid,f[111] = min(2, f[011] + 1) = min(2, 3) = 2
- Subset
010
: valid,f[111] = min(2, f[101] + 1) = min(2, 2) = 2
- Subset
001
: valid,f[111] = min(2, f[110] + 1) = min(2, 3) = 2
- Subset
Result: f[111] = 2
The minimum number of sessions needed is 2. One optimal arrangement:
- Session 1: Complete tasks 0 and 2 (durations 2+2 = 4 hours)
- Session 2: Complete task 1 (duration 3 hours)
Solution Implementation
1class Solution:
2 def minSessions(self, tasks: List[int], sessionTime: int) -> int:
3 n = len(tasks)
4
5 # Pre-compute which subsets of tasks can fit in a single session
6 # can_fit_in_session[mask] = True if tasks in mask can fit in one session
7 can_fit_in_session = [False] * (1 << n)
8
9 for mask in range(1, 1 << n):
10 # Calculate total time for tasks in current mask
11 total_time = sum(tasks[task_idx] for task_idx in range(n) if mask >> task_idx & 1)
12 can_fit_in_session[mask] = total_time <= sessionTime
13
14 # Dynamic programming array
15 # min_sessions[mask] = minimum sessions needed to complete tasks in mask
16 min_sessions = [float('inf')] * (1 << n)
17 min_sessions[0] = 0 # Base case: 0 sessions for 0 tasks
18
19 # Process each possible subset of tasks
20 for current_mask in range(1, 1 << n):
21 # Try all possible subsets of current_mask as a single session
22 subset = current_mask
23 while subset:
24 # If this subset can fit in one session
25 if can_fit_in_session[subset]:
26 # Update minimum sessions needed
27 # Sessions for current_mask = sessions for remaining tasks + 1
28 remaining_tasks = current_mask ^ subset
29 min_sessions[current_mask] = min(
30 min_sessions[current_mask],
31 min_sessions[remaining_tasks] + 1
32 )
33 # Get next subset of current_mask
34 subset = (subset - 1) & current_mask
35
36 # Return minimum sessions for all tasks (last element)
37 return min_sessions[-1]
38
1class Solution {
2 public int minSessions(int[] tasks, int sessionTime) {
3 int taskCount = tasks.length;
4
5 // Pre-compute which subsets of tasks can fit in a single session
6 // validSubset[mask] = true if the tasks represented by mask can fit in one session
7 boolean[] validSubset = new boolean[1 << taskCount];
8
9 // Check each possible subset of tasks
10 for (int mask = 1; mask < (1 << taskCount); mask++) {
11 int totalTime = 0;
12
13 // Calculate total time for current subset
14 for (int taskIndex = 0; taskIndex < taskCount; taskIndex++) {
15 // Check if task at taskIndex is included in current subset
16 if ((mask >> taskIndex & 1) == 1) {
17 totalTime += tasks[taskIndex];
18 }
19 }
20
21 // Mark subset as valid if it fits within session time limit
22 validSubset[mask] = (totalTime <= sessionTime);
23 }
24
25 // Dynamic programming array
26 // minSessionsDP[mask] = minimum sessions needed to complete tasks in mask
27 int[] minSessionsDP = new int[1 << taskCount];
28 Arrays.fill(minSessionsDP, Integer.MAX_VALUE / 2); // Initialize with large value
29 minSessionsDP[0] = 0; // Base case: 0 sessions for 0 tasks
30
31 // Process each possible state (subset of tasks)
32 for (int currentMask = 1; currentMask < (1 << taskCount); currentMask++) {
33 // Try all possible subsets of currentMask as a single session
34 for (int subset = currentMask; subset > 0; subset = (subset - 1) & currentMask) {
35 // If this subset can be completed in one session
36 if (validSubset[subset]) {
37 // Update minimum sessions: sessions for remaining tasks + 1 for current subset
38 int remainingTasks = currentMask ^ subset;
39 minSessionsDP[currentMask] = Math.min(
40 minSessionsDP[currentMask],
41 minSessionsDP[remainingTasks] + 1
42 );
43 }
44 }
45 }
46
47 // Return minimum sessions needed for all tasks (all bits set)
48 return minSessionsDP[(1 << taskCount) - 1];
49 }
50}
51
1class Solution {
2public:
3 int minSessions(vector<int>& tasks, int sessionTime) {
4 int n = tasks.size();
5
6 // isValidSession[mask] = true if the subset represented by mask can fit in one session
7 vector<bool> isValidSession(1 << n, false);
8
9 // Precompute which subsets of tasks can fit in a single session
10 for (int mask = 1; mask < (1 << n); ++mask) {
11 int totalTime = 0;
12
13 // Calculate total time for tasks in current subset
14 for (int taskIdx = 0; taskIdx < n; ++taskIdx) {
15 if ((mask >> taskIdx) & 1) { // Check if task at index taskIdx is included
16 totalTime += tasks[taskIdx];
17 }
18 }
19
20 // Mark subset as valid if it fits within session time limit
21 isValidSession[mask] = (totalTime <= sessionTime);
22 }
23
24 // dp[mask] = minimum number of sessions needed to complete tasks in subset mask
25 vector<int> dp(1 << n, INT_MAX);
26 dp[0] = 0; // Base case: no tasks require 0 sessions
27
28 // Dynamic programming to find minimum sessions for each subset
29 for (int mask = 1; mask < (1 << n); ++mask) {
30 // Try all possible subsets of current mask as last session
31 for (int subset = mask; subset > 0; subset = (subset - 1) & mask) {
32 // If this subset can fit in one session
33 if (isValidSession[subset]) {
34 // Update minimum sessions: sessions for remaining tasks + 1 for current subset
35 dp[mask] = min(dp[mask], dp[mask ^ subset] + 1);
36 }
37 }
38 }
39
40 // Return minimum sessions needed for all tasks (all bits set)
41 return dp[(1 << n) - 1];
42 }
43};
44
1/**
2 * Finds the minimum number of work sessions needed to complete all tasks
3 * @param tasks - Array of task durations
4 * @param sessionTime - Maximum duration of each work session
5 * @returns Minimum number of sessions needed
6 */
7function minSessions(tasks: number[], sessionTime: number): number {
8 const taskCount: number = tasks.length;
9
10 // Array to track which task combinations can fit in a single session
11 // Index represents a bitmask where bit i indicates if task i is included
12 const canFitInSession: boolean[] = new Array(1 << taskCount).fill(false);
13
14 // Pre-compute which task combinations can fit in a single session
15 for (let mask = 1; mask < (1 << taskCount); mask++) {
16 let totalTime: number = 0;
17
18 // Calculate total time for tasks in current mask
19 for (let taskIndex = 0; taskIndex < taskCount; taskIndex++) {
20 if (((mask >> taskIndex) & 1) === 1) {
21 totalTime += tasks[taskIndex];
22 }
23 }
24
25 // Mark if this combination fits within session time limit
26 canFitInSession[mask] = totalTime <= sessionTime;
27 }
28
29 // Dynamic programming array where dp[mask] represents minimum sessions
30 // needed to complete tasks indicated by the mask
31 const dp: number[] = new Array(1 << taskCount).fill(1 << 30);
32 dp[0] = 0; // Base case: no tasks require 0 sessions
33
34 // Process each possible task combination
35 for (let currentMask = 1; currentMask < (1 << taskCount); currentMask++) {
36 // Try all possible subsets of the current mask
37 // Iterate through all non-empty subsets efficiently
38 for (let subset = currentMask; subset > 0; subset = (subset - 1) & currentMask) {
39 // If this subset can fit in one session
40 if (canFitInSession[subset]) {
41 // Update minimum sessions: sessions for remaining tasks + 1 for current subset
42 dp[currentMask] = Math.min(dp[currentMask], dp[currentMask ^ subset] + 1);
43 }
44 }
45 }
46
47 // Return minimum sessions for all tasks (all bits set)
48 return dp[(1 << taskCount) - 1];
49}
50
Time and Space Complexity
Time Complexity: O(n × 3^n)
The algorithm consists of two main parts:
-
Preprocessing phase: Computing the
ok
array takesO(2^n × n)
time, as we iterate through all2^n
subsets and for each subset, we sum at mostn
tasks. -
Dynamic programming phase: The outer loop runs
O(2^n)
times (for each subseti
). For each subseti
, the inner while loop iterates through all subsetsj
ofi
. The key insight is that each element can be in one of three states relative to subsetsi
andj
:- In both
i
andj
(element must be inj
sincej
is a subset ofi
) - In
i
but not inj
- Not in
i
(and therefore not inj
)
This gives us
3^n
total iterations across all outer loop iterations, making the DP phaseO(3^n)
. - In both
Since O(3^n) > O(2^n × n)
, the overall time complexity is O(n × 3^n)
(the factor of n
comes from the sum operation in preprocessing).
Space Complexity: O(2^n)
The algorithm uses two arrays:
ok
array of size2^n
to store whether each subset can fit in one sessionf
array of size2^n
to store the minimum number of sessions for each subset
Both arrays have 2^n
elements, resulting in O(2^n)
space complexity.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Subset Enumeration Logic
A critical pitfall occurs when developers incorrectly implement the subset enumeration loop. The pattern j = (j - 1) & i
is subtle and easy to get wrong.
Incorrect Implementation:
# WRONG: This misses subset 0 and may cause infinite loop
j = i
while j > 0: # Wrong condition!
if ok[j]:
f[i] = min(f[i], f[i ^ j] + 1)
j = (j - 1) & i
Why it's wrong: When j
becomes 0, the loop exits immediately, but 0 is actually a valid subset (the empty subset). This causes the algorithm to miss valid transitions.
Correct Implementation:
# CORRECT: Process all subsets including empty set
j = i
while j:
if ok[j]:
f[i] = min(f[i], f[i ^ j] + 1)
j = (j - 1) & i
2. Forgetting to Check Empty Subset Validity
Another pitfall is not handling the empty subset case properly when precomputing valid sessions.
Problematic Code:
# This might cause issues if not careful
ok = [False] * (1 << n)
for i in range(1 << n): # Starting from 0
t = sum(tasks[j] for j in range(n) if i >> j & 1)
ok[i] = t <= sessionTime
Issue: ok[0]
will be set to True
(since sum of empty set is 0 ≤ sessionTime), but using the empty set as a "session" doesn't make logical sense and could lead to incorrect DP transitions.
Better Approach:
ok = [False] * (1 << n)
for i in range(1, 1 << n): # Start from 1, skip empty set
t = sum(tasks[j] for j in range(n) if i >> j & 1)
ok[i] = t <= sessionTime
3. Inefficient Bit Manipulation for Checking Set Membership
Developers might use inefficient methods to check if a task is in the current subset.
Inefficient:
# Converting to binary string and checking characters
binary_str = bin(mask)[2:].zfill(n)
for j in range(n):
if binary_str[n-1-j] == '1':
total_time += tasks[j]
Efficient:
# Direct bit manipulation
for j in range(n):
if mask >> j & 1:
total_time += tasks[j]
4. Not Understanding XOR Operation for Remaining Tasks
A conceptual pitfall is misunderstanding why i ^ j
gives the remaining tasks after completing subset j
.
Wrong Mental Model:
# WRONG: Thinking subtraction works for removing subsets remaining = i - j # This doesn't work for bit masks!
Correct Understanding:
# CORRECT: XOR removes the bits that are in j from i remaining = i ^ j # Flips all bits in j, effectively removing them from i
Example: If i = 0b111
(tasks 0,1,2) and j = 0b101
(tasks 0,2), then i ^ j = 0b010
(task 1 remains).
5. Memory Optimization Oversight
While not incorrect, failing to recognize potential memory optimizations can be problematic for larger inputs.
Space-Heavy Approach:
# Storing all possible session combinations
valid_sessions = []
for mask in range(1, 1 << n):
if total_time <= sessionTime:
valid_sessions.append(mask)
Better Approach: The original solution's boolean array is more memory-efficient, using only 1 bit of information per subset rather than storing the actual subset values.
A heap is a ...?
Recommended Readings
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
Backtracking Template Prereq DFS with States problems dfs_with_states Combinatorial search problems Combinatorial search problems involve finding groupings and assignments of objects that satisfy certain conditions Finding all permutations combinations subsets and solving Sudoku are classic combinatorial problems The time complexity of combinatorial problems often grows rapidly with the size of
Bitmask and Dynamic Programming Bit manipulation is a crucial aspect of computer programming and one of the most powerful tools for bit manipulation is bitmasks Let's first understand what a bit is A bit is a binary digit It's the smallest piece of data in a computer and can be
Want a Structured Path to Master System Design Too? Don’t Miss This!