Task Scheduler

Hero Image

Problem Overview

The "Task Scheduler" problem is a classic problem in scheduling that can be solved using heaps, an essential data structure. Given a set of tasks represented by characters, and a non-negative integer n representing the cooldown period between two same tasks, the objective is to determine the minimum number of units of time required to execute all the given tasks in a way that respects the cooldown period between identical tasks.

Understanding the Problem

Consider a scenario where you have several tasks, each represented by a character (e.g., A, B, C), and you must schedule these tasks with a cooling interval n. This means you must wait for n intervals before executing the same task again. The challenge is to find an algorithm that minimizes the total time needed to finish all tasks.

Example

  • Input: tasks = ["A","A","A","B","B","B"], n = 2
  • Output: 8
  • Explanation: An optimal scheduling order is A -> B -> idle -> A -> B -> idle -> A -> B.

Key DSA Concepts and Theory

Heaps

A heap is a special tree-based data structure that satisfies the heap property. In a max heap, for example, the key of a parent node is always greater than or equal to the keys of its children. Heaps are commonly implemented as binary trees and used in priority queue operations.

Priority Queues

A priority queue is an extended version of a regular queue with each element having a priority associated with it. Elements are dequeued in order of their priority, rather than their order in the queue. In this problem, a max priority queue can be used to always fetch the task with the highest pending execution count.

Solution Approach

The goal is to simulate the scheduling process using a priority queue to manage task execution based on the remaining count of each task.

  1. Count Frequency of Tasks: First, count the occurrences of each task and store them.

  2. Use a Max Heap to Manage Tasks: Insert each task into a max heap (priority queue) based on its frequency. This ensures that we always schedule the task with the maximum remaining count first.

  3. Schedule Tasks: Use a loop to schedule tasks based on their frequencies. Between two consecutive identical tasks, insert idle times if necessary.

  4. Repeat until All Tasks are Processed: Continue popping tasks from the heap and recording the time taken until all tasks are scheduled.

Here's how you can implement this in C++ and Java:

C++ Code

#include <vector>
#include <queue>
#include <unordered_map>

int leastInterval(std::vector<char>& tasks, int n) {
    std::unordered_map<char, int> taskCount;
    for (char task : tasks) {
        taskCount[task]++;
    }

    std::priority_queue<int> maxHeap;
    for (auto& pair : taskCount) {
        maxHeap.push(pair.second);
    }

    int intervals = 0;
    while (!maxHeap.empty()) {
        int time = 0;
        std::vector<int> temp;
        
        for (int i = 0; i <= n; ++i) {
            if (!maxHeap.empty()) {
                temp.push_back(maxHeap.top() - 1);
                maxHeap.pop();
                ++time;
            }
        }
        
        for (int count : temp) {
            if (count > 0) {
                maxHeap.push(count);
            }
        }
        
        intervals += maxHeap.empty() ? time : n + 1;
    }
    return intervals;
}

Java Code

import java.util.*;

public class TaskScheduler {
    public int leastInterval(char[] tasks, int n) {
        Map<Character, Integer> taskCount = new HashMap<>();
        for (char task : tasks) {
            taskCount.put(task, taskCount.getOrDefault(task, 0) + 1);
        }

        PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Collections.reverseOrder());
        maxHeap.addAll(taskCount.values());

        int intervals = 0;
        while (!maxHeap.isEmpty()) {
            int time = 0;
            List<Integer> temp = new ArrayList<>();
            
            for (int i = 0; i <= n; ++i) {
                if (!maxHeap.isEmpty()) {
                    temp.add(maxHeap.remove() - 1);
                    ++time;
                }
            }
            
            for (int count : temp) {
                if (count > 0) {
                    maxHeap.add(count);
                }
            }
            
            intervals += maxHeap.isEmpty() ? time : n + 1;
        }
        return intervals;
    }
}

Time and Space Complexity Analysis

  • Time Complexity: O(N + M*logM) where N is the number of tasks and M is the number of distinct tasks. The reason for M*logM is because each pop/push operation in the heap costs O(logM) and might be iterated multiple times for different tasks.
  • Space Complexity: O(M) for the task counter and max heap, where M is the number of distinct tasks.

Common Mistakes to Avoid

  • Ignoring Idle Time: Do not ignore idle times when no task can execute due to cooldown periods.
  • Incorrect Heap Management: Ensure that the highest count task is always scheduled first and properly manage the reduction of task frequencies.

Similar Problems on LeetCode

  • 621. Task Scheduler: Another variation on task scheduling problems.
  • 358. Rearrange String k Distance Apart: Similar scheduling strategies but applied to string rearrangement.

Additional Resources and References

  • Heaps and Priority Queues in Java Documentation.
  • Visualization of how priority queues work can be vital in understanding and solving this problem efficiently.
  • Practice using different types of heaps, such as min-heaps and max-heaps, to understand their applicability in various scenarios.

Understanding these concepts and practicing the problem through coding can enhance your problem-solving skills in not only competitive programming but also practical scheduling problems.