Skip to content

Latest commit

 

History

History
 
 

621

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 

Given a characters array tasks, representing the tasks a CPU needs to do, where each letter represents a different task. Tasks could be done in any order. Each task is done in one unit of time. For each unit of time, the CPU could complete either one task or just be idle.

However, there is a non-negative integer n that represents the cooldown period between two same tasks (the same letter in the array), that is that there must be at least n units of time between any two same tasks.

Return the least number of units of times that the CPU will take to finish all the given tasks.

 

Example 1:

Input: tasks = ["A","A","A","B","B","B"], n = 2
Output: 8
Explanation: 
A -> B -> idle -> A -> B -> idle -> A -> B
There is at least 2 units of time between any two same tasks.

Example 2:

Input: tasks = ["A","A","A","B","B","B"], n = 0
Output: 6
Explanation: On this case any permutation of size 6 would work since n = 0.
["A","A","A","B","B","B"]
["A","B","A","B","A","B"]
["B","B","B","A","A","A"]
...
And so on.

Example 3:

Input: tasks = ["A","A","A","A","A","A","B","C","D","E","F","G"], n = 2
Output: 16
Explanation: 
One possible solution is
A -> B -> C -> A -> D -> E -> A -> F -> G -> A -> idle -> idle -> A -> idle -> idle -> A

 

Constraints:

  • 1 <= task.length <= 104
  • tasks[i] is upper-case English letter.
  • The integer n is in the range [0, 100].

Companies:
Facebook, Microsoft, Uber, Amazon, Rubrik, Bloomberg

Related Topics:
Array, Hash Table, Greedy, Sorting, Heap (Priority Queue), Counting

Similar Questions:

Solution 1. Greedy

Use a queue to simulate the cooldown process. Use a max heap to pick the character with the greatest count left.

// OJ: https://leetcode.com/problems/task-scheduler/
// Author: github.com/lzl124631x
// Time: O(N + MlogC + K) where `M` is the length of `tasks`, `C` is the size of the character set, `K` is the number of idle CPU times used in the optimal solution.
// Space: O(N)
class Solution {
public:
    int leastInterval(vector<char>& A, int n) {
        if (n == 0) return A.size();
        int used = 0, cnt[26] = {}, ans = 0;
        auto cmp = [&](int a, int b) { return cnt[a] < cnt[b]; };
        priority_queue<int, vector<int>, decltype(cmp)> pq(cmp); // max-cnt heap of tasks available to be executed.
        for (char c : A) cnt[c - 'A']++;
        for (int i = 0; i < 26; ++i) {
            if (cnt[i]) pq.push(i);
        }
        queue<int> q; // `q` is a cool down queue of length n+1.
        for (int i = 0; i <= n; ++i) q.push(-1); // -1s in the cooldown queue are just placeholders, meaning no task was executed at that particular moment
        while (used < A.size()) {
            int c = q.front();
            q.pop();
            if (c != -1) pq.emplace(c); // if a task is popped from cooldown queue, this task can be used now.
            if (pq.size()) { // we have tasks available
                c = pq.top(); // greedily pick the task `c` with the max cnt
                pq.pop();
                ++used; // execute task `c`
                q.push(--cnt[c] > 0 ? c : -1); // push task `c` into cooldown queue if it still have remaining quota. Otherwise put a placeholder -1 into cooldown queue
            } else q.push(-1); // no tasks available. Put a placeholder into the cooldown queue.
            ++ans;
        }
        return ans;
    }
};

Solution 2. Greedy

Let time[i] be the available time of task labelled by 'A' + i.

We pick tasks for tasks.length time. Each time, we greedily pick the task with the earliest available time. In case of a tie, we pick the task with the greatest count left.

// OJ: https://leetcode.com/problems/task-scheduler/
// Author: github.com/lzl124631x
// Time: O(MC) where M is the length of `tasks` and `C` is the size of the character set.
// Space: O(C)
class Solution {
public:
    int leastInterval(vector<char>& A, int n) {
        int cnt[26] = {}, time[26] = {}, ans = 0;
        for (char c : A) cnt[c - 'A']++;
        for (int i = 0; i < A.size(); ++i) {
            int k = -1;
            ++ans;
            for (int j = 0; j < 26; ++j) {
                if (cnt[j] == 0) continue;
                if (time[j] < ans) time[j] = ans; // the available time for task `j` is at least the current time.
                if (k == -1 || time[j] < time[k] || (time[j] == time[k] && cnt[j] > cnt[k])) k = j; // we pick the task with the earliest available time. If the available times are the same, we pick the task with the greatest count left.
            }
            ans = time[k]; // jump to the available time of task labelled by `'A' + k`
            --cnt[k];
            time[k] += n + 1;
        }
        return ans;
    }
};

Solution 3. Greedy

The key is to find out how many idles we need.

It's not hard to figure out that we need to do a greedy arrangment -- always arrange tasks with the greatest frequency first.

For example, we have the following tasks: 3A, 2B, 1C. And n = 2. We should arrange A, then B, and C.

// Arrange task A. `?` stands for empty slot
A ? ? A ? ? A
// Arrange task B and C. `#` stands for idle slot
A B C A B # A

The answer is the number of idle slots + number of tasks.

After arranging A, A separated slots into partCnt = count(A) - 1 = 2 parts, each of which has length n. Now we know the number of empty slots emptySlots = partCnt * n. Then we can get how many tasks we have to put into those slots: availableTasks = tasks.length - count(A).

If emptySlots > availableTasks, we don't have enough tasks to fill all the empty slots. We must use idle = max(0, emptySlots - availableTasks) idle slots.

Now consider a special case: what if there are more than one task with the greatest frequency? Example: 3A, 3B, 2C, 1D, n = 3.

// Arrange A
A ? ? ? A ? ? ? A
// Arrange B
A B ? ? A B ? ? A B
// We can encode this as
X ? ? X ? ? X

We can see that the only change is that each part has length 2 instead of 3 now.

Now we get the generalized equations:

partCnt = count(A) - 1
emptySlots = (n - num + 1) * partCnt // `num` is the count of tasks with the greatest frequency
availableTasks = tasks.length - count(A) * num
idleCnt = max(0, emptySlots - availableTasks)
answer = tasks.length + idleCnt

What if we have more than n tasks with the greatest frequency and we got emptySlots negative? Example: 3A, 3B, 3C, 3D, 2E, 1F, n = 2.

// Arrange A, B, C, D
A B C D | A B C D | A B C D
// Arrange E and F
A B C D E F | A B C D E | A B C D

We can see that, if we have greater than or equal to n + 1 tasks with the greatest frequency, each part will be filled completely and there won't be empty slots. We can freely fill the rest tasks one by one in each part, because they automatically satisfies the n cooldown rule.


Test case:

["A", "A", "A", "B", "B", "C", "C", "D", "D"]
2
// Arrange A
A ? ? | A ? ? | A
// Arrange B, C and D
A B C D | A B C D | A
partCnt = 3 - 1 = 2
emptySlots = (2 - 1 + 1) * 2 = 4 
availableTasks = 9 - 3 * 1 = 6
idleCnt = max(0, 4 - 6) = 0
answer = 9 + 0 = 0
// OJ: https://leetcode.com/problems/task-scheduler/
// Author: github.com/lzl124631x
// Time: O(M) where M is the length of `tasks`
// Space: O(C)
// Ref: https://leetcode.com/problems/task-scheduler/discuss/104500/Java-O(n)-time-O(1)-space-1-pass-no-sorting-solution-with-detailed-explanation
class Solution {
public:
    int leastInterval(vector<char>& A, int n) {
        int cnt[26] = {}, maxCnt = 0, num = 0;
        for (int n : A) maxCnt = max(maxCnt, ++cnt[n - 'A']);
        for (int n : cnt) num += n == maxCnt;
        int partCnt = maxCnt - 1;
        int emptySlots = (n - num + 1) * partCnt;
        int availableTasks = A.size() - maxCnt * num;
        int idleCnt = max(0, emptySlots - availableTasks);
        return A.size() + idleCnt;
    }
};

Solution 4. Greedy

Similar to solution 3, here we directly calculate the number of units of times.

Let mxCnt be the maximum frequency of A[i]. mxCntFreq be the number of unique tasks with mxCnt frequency.

We always greedily place the tasks with mxCnt frequency first.

For example, A = "AAABBBC", n = 2. mxCnt = 3 and mxCntFreq = 2. We place A and B first.

A B - A B - A B

Then we place other tasks. In this case, we just need to place C.

A B C A B - A B

If mxCntFreq >= n + 1, it means that we can fill all the sections of length n + 1 with mxCnt frequency tasks. We can place the other tasks between the sections without leaving any idle item.

If mxCntFreq < n + 1, we need to fill the gap = (mxCnt - 1) * (n + 1 - mxCntFreq) gaps with the other otherFreq = A.size() - mxCntFreq * mxCnt tasks. The answer is mxCntFreq * mxCnt + max(otherFreq, gap).

// OJ: https://leetcode.com/problems/task-scheduler/
// Author: github.com/lzl124631x
// Time: O(M) where M is the length of `tasks`
// Space: O(C)
class Solution {
public:
    int leastInterval(vector<char>& A, int n) {
        int cnt[26] = {};
        for (char c : A) cnt[c - 'A']++;
        int mxCnt = *max_element(begin(cnt), end(cnt)), mxCntFreq = 0;
        for (int n : cnt) mxCntFreq += n == mxCnt;
        if (mxCntFreq >= n + 1) return A.size();
        int ans = mxCntFreq * mxCnt, otherFreq = A.size() - ans, gap = (mxCnt - 1) * (n + 1 - mxCntFreq);
        return ans + max(otherFreq, gap);
    }
};