Problem Challenge 2
Resources for scheduling tasks on a server.
Problem statement
You are given a list of tasks that need to be run, in any order, on a server. Each task will take one CPU interval to execute but once a task has finished, it has a cooling period during which it can’t be run again. If the cooling period for all tasks is ‘K’ intervals, find the minimum number of CPU intervals that the server needs to finish all tasks.
If at any time the server can’t execute any task then it must stay idle.
Dry-run templates
This is the implementation of the solution provided in the lesson:
from heapq import *def schedule_tasks(tasks, k):intervalCount = 0taskFrequencyMap = {}for char in tasks:taskFrequencyMap[char] = taskFrequencyMap.get(char, 0) + 1maxHeap = []# add all tasks to the max heapfor char, frequency in taskFrequencyMap.items():heappush(maxHeap, (-frequency, char))while maxHeap:waitList = []n = k + 1 # try to execute as many as 'k+1' tasks from the max-heapwhile n > 0 and maxHeap:intervalCount += 1frequency, char = heappop(maxHeap)if -frequency > 1:# decrement the frequency and add to the waitListwaitList.append((frequency+1, char))n -= 1# put all the waiting list back on the heapfor frequency, char in waitList:heappush(maxHeap, (frequency, char))if maxHeap:intervalCount += n # we'll be having 'n' idle intervals for the next iterationreturn intervalCountdef main():print("Minimum intervals needed to execute all tasks: " +str(schedule_tasks(['a', 'a', 'a', 'b', 'c', 'c'], 2)))print("Minimum intervals needed to execute all tasks: " +str(schedule_tasks(['a', 'b', 'a'], 3)))main()
This problem follows the top ‘K’ elements pattern. We need to rearrange tasks such that the same tasks are ‘K’ distance apart.
Consider an example where tasks are and k
is 2. This means, that the same task cannot execute before 2 CPU intervals. Therefore, the scheduling for the above tasks would be: . As evident, after the first , the next is scheduled after two tasks, that is, after two CPU intervals.
However, if the given tasks were and k
was still 2, the scheduling would be , instead.
Sample input #1
Input: [a, a, a, b, c, c], K=2
Note: The first
for
loop populates thetaskFrequencyMap
dictionary with tasks and their frequencies. The secondfor
loop pushes them to the heap. Our table below does not show the iterations for these loops. The outer iteration column refers to thewhile
loop in our code, inner while loop iteration refers to the nestedwhile
loop, and the inner for loop iteration to the nestedfor
loop.
Lines 6-8 in the above code populate our taskFrequencyMap
dictionary with the tasks and their frequencies. For this example, the dictionary will be {'a': 3, 'b': 1, 'c': 2}
. This step is not shown in the table below.
Outer iteration | Inner while loop iteration | Inner for loop iteration | Line number | MaxHeap | n | interval count | waitList |
- | - | - | 5, 6, 10 | [] | - | 0 | - |
- | - | - | 7-8 | [] | - | 0 | - |
- | - | - | 12-13 | [(-3, 'a'), (-1, 'b'), (-2, 'c')] | - | 0 | - |
1 | - | - | 16-17 | [(-3, 'a'), (-1, 'b'), (-2, 'c')] | 3 | 0 | [] |
1 | 1 | - | 17-24 | [(-2, 'c'), (-1, 'b')] | 2 | 1 | [(-2, 'a')] |
1 | 2 | - | 17-24 | [(-1, 'b')] | 1 | 2 | [(-2, 'a'), (-1, 'c')] |
1 | 3 | - | 17-24 | [] | 0 | 3 | [(-2, 'a'), (-1, 'c')] |
1 | - | 1 | 27-28 | [(-2, 'a')] | 0 | 3 | [(-2, 'a'), (-1, 'c')] |
1 | - | 2 | 27-28 | [(-2, 'a'), (-1, 'c')] | 0 | 3 | [(-2, 'a'), (-1, 'c')] |
1 | - | - | 30-31 | [(-2, 'a'), (-1, 'c')] | 0 | 3 | [(-2, 'a'), (-1, 'c')] |
2 | - | - | 16-17 | [(-2, 'a'), (-1, 'c')] | 3 | 3 | [] |
2 | 1 | - | 17-24 | [(-1, 'c')] | 2 | 4 | [(-1, 'a')] |
2 | 2 | - | 17-24 | [] | 1 | 5 | [(-1, 'a')] |
2 | - | 1 | 27-28 | [(-1, 'a')] | 1 | 6 | [(-1, 'a')] |
3 | - | - | 16-17 | [(-1, 'a')] | 3 | 6 | [] |
3 | 1 | - | 17-24 | [] | 2 | 7 | [] |
Sample input #2
Input: [a, b, a], K=3
Lines 6-8 in the above code populate our taskFrequencyMap
dictionary with the tasks and their frequencies. For this example, the dictionary will be {'a': 2, 'b': 1}
. This step is not shown in the table below.
Outer iteration | Inner while loop iteration | Inner for loop iteration | Line number | MaxHeap | n | interval count | waitList |
- | - | - | 5, 6, 10 | [] | - | 0 | - |
- | - | - | 12-13 | [(-2, 'a'), (-1, 'b')] | - | 0 | - |
1 | - | - | 16-17 | [(-2, 'a'), (-1, 'b')] | 4 | 0 | [] |
1 | 1 | - | 17-24 | [(-1, 'b')] | 3 | 1 | [(-1, 'a')] |
1 | 2 | - | 17-24 | [] | 2 | 2 | [(-1, 'a')] |
1 | - | 1 | 27-28 | [(-1, 'a')] | 2 | 2 | [(-1, 'a')] |
1 | - | - | 30-31 | [(-1, 'a')] | 2 | 4 | [(-1, 'a')] |
2 | - | - | 16-17 | [(-1, 'a')] | 4 | 4 | [] |
2 | 1 | - | 17-24 | [] | 3 | 5 | [] |
Step-by-step solution construction
Here’s what we’ll be doing:
- We will use a Max Heap to execute the highest frequency task first.
- After executing a task we decrease its frequency and put it on a waiting list.
- In each iteration, we will try to execute as many as
k+1
tasks. - For the next iteration, we will put all the waiting tasks back in the Max Heap.
- If for any iteration, we are not able to execute
k+1
tasks, the CPU has to remain idle for the remaining time in the next iteration.
The first step is to populate our taskFrequencyMap
dictionary with the tasks and their frequencies. We use the get
function to get a task’s frequency from the dictionary and update its value by incrementing it by 1.
from heapq import *def schedule_tasks(tasks, k):print("Given tasks: ", tasks)print("Given interval: ", k)print("")intervalCount = 0taskFrequencyMap = {}i = 1print("Populating our frequency dictionary.")for char in tasks:print("\tLoop iteration: ", i, sep = "")print("\t\tTask: ", char, sep="")taskFrequencyMap[char] = taskFrequencyMap.get(char, 0) + 1print("\t\ttaskFrequencyMap: ", taskFrequencyMap, "\n", sep = "")def main():input = [(['a', 'a', 'a', 'b', 'c', 'c'], 2), (['a', 'b', 'a'], 3)]for i in input:print("Minimum intervals needed to execute all tasks: " +str(schedule_tasks(i[0], i[1])))print("-"*100, "\n", sep="")main()
The next step is to push the tasks and their frequencies to our maxHeap
. We’ll add them as tuples and since we’re maintaining a max heap, we’ll make the frequencies negative. The heapq
library will use the first element of the tuple as the key for organizing the tuples as a heap. This way, we’ll always have the largest frequency at the top of the heap, that is, its first value.
from heapq import *def schedule_tasks(tasks, k):print("Given tasks: ", tasks)print("Given interval: ", k)print("")intervalCount = 0taskFrequencyMap = {}i = 1print("Populating our frequency dictionary.")for char in tasks:print("\tLoop iteration: ", i, sep = "")print("\t\tTask: ", char, sep="")taskFrequencyMap[char] = taskFrequencyMap.get(char, 0) + 1print("\t\ttaskFrequencyMap: ", taskFrequencyMap, "\n", sep = "")maxHeap = []# add all tasks to the max heapj = 1print("Populating our maxHeap.")for char, frequency in taskFrequencyMap.items():print("\tLoop iteration: ", j, sep = "")j+=1print("\t\tPushing (", frequency, ", ", char, ") into the heap.", sep = "")heappush(maxHeap, (-frequency, char))print("\t\tMaxHeap: ", maxHeap, "\n", sep = "")def main():input = [(['a', 'a', 'a', 'b', 'c', 'c'], 2), (['a', 'b', 'a'], 3)]for i in input:print("Minimum intervals needed to execute all tasks: " +str(schedule_tasks(i[0], i[1])))print("-"*100, "\n", sep="")main()
Next, we’ll start executing our tasks and pushing them into the waitlist if there are multiple occurrences. We’ll try to execute k+1
tasks. We’ll pop the task from our maxHeap
since now we’ll have one less task to consider.
If a task’s frequency is greater than 1, we’ll execute it once and then decrement its frequency to append to the waitlist. This is because the same task cannot execute before k
intervals and now has to wait. Lastly, we’ll decrement our n
since now we have one less task to execute.
from heapq import *def schedule_tasks(tasks, k):print("Given tasks: ", tasks)print("Given interval: ", k)print("")intervalCount = 0taskFrequencyMap = {}i = 1print("Populating our frequency dictionary.")for char in tasks:print("\tLoop iteration: ", i, sep = "")print("\t\tTask: ", char, sep="")taskFrequencyMap[char] = taskFrequencyMap.get(char, 0) + 1print("\t\ttaskFrequencyMap: ", taskFrequencyMap, "\n", sep = "")maxHeap = []# add all tasks to the max heapj = 1print("Populating our maxHeap.")for char, frequency in taskFrequencyMap.items():print("\tLoop iteration: ", j, sep = "")j+=1print("\t\tPushing (", frequency, ", ", char, ") into the heap.", sep = "")heappush(maxHeap, (-frequency, char))print("\t\tMaxHeap: ", maxHeap, "\n", sep = "")while maxHeap:waitList = []n = k + 1 # try to execute as many as 'k+1' tasks from the max-heapprint("We'll try to execute as many as ", k + 1, " tasks." , sep = "")x = 1while n > 0 and maxHeap:print("\tLoop iteration: ", x, sep = "")x+=1intervalCount += 1frequency, char = heappop(maxHeap)print("\t\t(", frequency, ", ", char, ") popped from the heap.", sep ="")if -frequency > 1:# decrement the frequency and add to the waitListprint("\t\tSince the frequency of the task is greater than 1, decrementing it and appending to our waitlist.")waitList.append((frequency+1, char))else:print("\t\tSince the frequency of our task was 1, we don't need to append it to our waitlist.")n -= 1print("\t\tWaitlist: ", waitList, "\n", sep = "")def main():input = [(['a', 'a', 'a', 'b', 'c', 'c'], 2), (['a', 'b', 'a'], 3)]for i in input:print("Minimum intervals needed to execute all tasks: " +str(schedule_tasks(i[0], i[1])))print("-"*100, "\n", sep="")main()
Once our maxHeap
is empty, this means we’ve executed our tasks one time and the remaining have been appended to the waitlist. The next step is to now put back the waitlisted tasks in the heap to be executed again.
from heapq import *def schedule_tasks(tasks, k):print("Given tasks: ", tasks)print("Given interval: ", k)print("")intervalCount = 0taskFrequencyMap = {}i = 1print("Populating our frequency dictionary.")for char in tasks:print("\tLoop iteration: ", i, sep = "")print("\t\tTask: ", char, sep="")taskFrequencyMap[char] = taskFrequencyMap.get(char, 0) + 1print("\t\ttaskFrequencyMap: ", taskFrequencyMap, "\n", sep = "")maxHeap = []# add all tasks to the max heapj = 1print("Populating our maxHeap.")for char, frequency in taskFrequencyMap.items():print("\tLoop iteration: ", j, sep = "")j+=1print("\t\tPushing (", frequency, ", ", char, ") into the heap.", sep = "")heappush(maxHeap, (-frequency, char))print("\t\tMaxHeap: ", maxHeap, "\n", sep = "")while maxHeap:waitList = []n = k + 1 # try to execute as many as 'k+1' tasks from the max-heapprint("We'll try to execute as many as ", k + 1, " tasks." , sep = "")x = 1while n > 0 and maxHeap:print("\tLoop iteration: ", x, sep = "")x+=1intervalCount += 1frequency, char = heappop(maxHeap)print("\t\t(", frequency, ", ", char, ") popped from the heap.", sep ="")if -frequency > 1:# decrement the frequency and add to the waitListprint("\t\tSince the frequency of the task is greater than 1, decrementing it and appending to our waitlist.")waitList.append((frequency+1, char))else:print("\t\tSince the frequency of our task was 1, we don't need to append it to our waitlist.")n -= 1print("\t\tWaitlist: ", waitList, "\n", sep = "")# put all the waiting list back on the heapprint("Pushing back our waitlisted tasks to the heap.")print("Waitlist: ", waitList, sep = "")c = 1for frequency, char in waitList:print("\tLoop iteration: ", c, sep = "")c+=1print("\t\tPushing (", frequency, ", ", char, ") to the heap.", sep = "")heappush(maxHeap, (frequency, char))print("\t\tmaxHeap: ", maxHeap, "\n", sep = "")def main():input = [(['a', 'a', 'a', 'b', 'c', 'c'], 2), (['a', 'b', 'a'], 3)]for i in input:print("Minimum intervals needed to execute all tasks: " +str(schedule_tasks(i[0], i[1])))print("-"*100, "\n", sep="")main()
Lastly, we’ll add an if
condition to check if there are still tasks remaining in our maxHeap. If yes, we’ll increment our interval count by n
. That is, we’ll be having n
idle intervals for the next iteration.
We’ll continue in the same loop until our waitlist and max heap are empty. That is, all tasks have been executed. Then, we’ll return the final interval count.
from heapq import *orderlist = []def schedule_tasks(tasks, k):print("Given tasks: ", tasks)print("Given interval: ", k)print("")intervalCount = 0taskFrequencyMap = {}i = 1print("Populating our frequency dictionary.")for char in tasks:print("\tLoop iteration: ", i, sep = "")print("\t\tTask: ", char, sep="")taskFrequencyMap[char] = taskFrequencyMap.get(char, 0) + 1print("\t\ttaskFrequencyMap: ", taskFrequencyMap, "\n", sep = "")maxHeap = []# add all tasks to the max heapj = 1print("Populating our maxHeap.")for char, frequency in taskFrequencyMap.items():print("\tLoop iteration: ", j, sep = "")j+=1print("\t\tPushing (", frequency, ", ", char, ") into the heap.", sep = "")heappush(maxHeap, (-frequency, char))print("\t\tMaxHeap: ", maxHeap, "\n", sep = "")while maxHeap:waitList = []n = k + 1 # try to execute as many as 'k+1' tasks from the max-heapprint("We'll try to execute as many as ", k + 1, " tasks." , sep = "")x = 1while n > 0 and maxHeap:print("\tLoop iteration: ", x, sep = "")x+=1intervalCount += 1frequency, char = heappop(maxHeap)global orderlistorderlist.append(char)print("\t\t(", frequency, ", ", char, ") popped from the heap.", sep ="")if -frequency > 1:# decrement the frequency and add to the waitListprint("\t\tSince the frequency of the task is greater than 1, decrementing it and appending to our waitlist.")waitList.append((frequency+1, char))print("\t\tWaitlist: ", waitList, "\n", sep = "")else:print("\t\tSince the frequency of our task was 1, we don't need to append it to our waitlist.")n -= 1print("\t\tWaitlist: ", waitList, "\n", sep = "")# put all the waiting list back on the heapprint("Pushing back our waitlisted tasks to the heap.")print("Waitlist: ", waitList, sep = "")c = 1for frequency, char in waitList:print("\tLoop iteration: ", c, sep = "")c+=1print("\t\tPushing (", frequency, ", ", char, ") to the heap.", sep = "")heappush(maxHeap, (frequency, char))print("\t\tmaxHeap: ", maxHeap, "\n", sep = "")if maxHeap:print("Our max heap is not empty, hence, we'll increment our interval count by n = ", str(n), ".", sep="")intervalCount += n # we'll be having 'n' idle intervals for the next iterationfor i in range(n):orderlist.append("idle")print("\tInterval count: ", intervalCount, "\n", sep = "")else:print("Our max heap is empty, no more tasks to execute!\n")return intervalCountdef main():input = [(['a', 'a', 'a', 'b', 'c', 'c'], 2), (['a', 'b', 'a'], 3)]for i in input:print("Minimum intervals needed to execute all tasks: " +str(schedule_tasks(i[0], i[1])))outputstr = ""global orderlistfor i in orderlist:outputstr += i + " --> "print("Order of task execution: ", outputstr[:-5])print("-"*100, "\n", sep="")orderlist.clear()main()