Facebook Pixel

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:

  1. 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
  2. You can start a new task immediately after finishing the previous one - there's no gap needed between tasks within the same session
  3. 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:

  1. Try assigning each task to an existing work session if it fits
  2. If no existing session can accommodate the task, create a new session
  3. Recursively solve for the remaining tasks
  4. Backtrack and try different assignments to find the minimum number of sessions
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. What combinations of tasks can fit in a single session?
  2. 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 size 2^n where ok[i] indicates whether the subset represented by binary number i can fit in one session
  • For each subset i, we calculate the total time by checking each bit: if bit j is set (i >> j & 1), we include tasks[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 subsets j
  • The subset enumeration uses the technique j = (j - 1) & i:
    • Starting with j = i, this generates all subsets of i in descending order
    • The operation (j - 1) & i moves to the next smaller subset
    • When j becomes 0, we stop
  • For each valid session j (where ok[j] is true), we try using it:
    • i ^ j represents the remaining tasks after completing session j
    • 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 Evaluator

Example 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
  • State 010 (task 1):

    • Subset 010: valid session, f[010] = f[000] + 1 = 0 + 1 = 1
  • 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
  • State 100 (task 2):

    • Subset 100: valid session, f[100] = f[000] + 1 = 0 + 1 = 1
  • 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
  • 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
  • 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

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:

  1. Preprocessing phase: Computing the ok array takes O(2^n × n) time, as we iterate through all 2^n subsets and for each subset, we sum at most n tasks.

  2. Dynamic programming phase: The outer loop runs O(2^n) times (for each subset i). For each subset i, the inner while loop iterates through all subsets j of i. The key insight is that each element can be in one of three states relative to subsets i and j:

    • In both i and j (element must be in j since j is a subset of i)
    • In i but not in j
    • Not in i (and therefore not in j)

    This gives us 3^n total iterations across all outer loop iterations, making the DP phase O(3^n).

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 size 2^n to store whether each subset can fit in one session
  • f array of size 2^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.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

A heap is a ...?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More