5 CPU's Task scheduling N process

2020-06-06 05:54发布

问题:

Question.

There are 5 CPUs and N number of tasks in the queue. You have to use minimum CPUs to process the tasks.

A task is of format [arrival time, time to process the task].

Note:

  1. You can only use at most 5 CPUs. If it is not possible in 5 CPUs, print -1.

  2. The time to process the task should not be greater than 10 i.e (Time waiting in queue + time to process the task) <= 10.

  3. If to process the current task, you need more than 10 seconds in current CPU, you can move to a different CPU and check if it is possible to process the task in <=10 time.

  4. If it is not possible to process the task in <=10 or at most 5 CPUs, print -1.

Constraints.

0 <= Arrival time <= 500

1 <= time to process the task <= 10

0 <= N <=500

You can only use iostream library. No STL's are allowed.

Time : 3 second for T test cases

Eg:-

Input

3

1 6

2 7

3 1

Output

2

Explanation:

3 - N

1 6 - the first task arrives in CPU0 at time 1, and leaves at time 7 (1+6). CPUs used = 1.

2 7 - the second task arrives in CPU0 at time 2, and wait for 5 seconds in the queue, so overall processing time is 5+7 > 10. So it is moved to CPU1. CPUs used = 2.

3 1 - the third task arrives. it can go to CPU0 or CPU1, as processing time is 5 ( (7-3) + 1 ) and 7 ( (9-3) + 1 ) seconds respectively. CPUs used = 2.

CPU1 is a fresh CPU. So task2 will be completed in 9 (2 + 7) seconds without any Time to wait in the queue.

My Approach:

  1. Initially, thought this as a variant of minimum train-platform problem. But that was wrong.

  2. Tried a greedy approach, but it gave -1 for some valid solutions.

CODE:

#include <iostream>
using namespace std;

#define MAXN 500

int N;
int arr[MAXN];
int len[MAXN];

int main(){

    ios_base::sync_with_stdio(false);
    cin.tie(NULL);

    cin>>N;

    for(int i=0;i<N;i++){
        cin>> arr[i] >> len[i];
    }


    int exit_time_of_task_in_CPUs[5]={0};
    int cpus_used_so_far=1;

    int min_processing_time;
    int min_processing_time_idx;

    for(int task_idx=0; task_idx<N; task_idx++){

        min_processing_time = INT_MAX;
        min_processing_time_idx = -1;

        // finds the CPU which can process task in minimum time.
        for(int i=0; i<cpus_used_so_far ; i++){

            int processing_time = 0;
            int time_in_queue = exit_time_of_task_in_CPUs[i] - arr[task_idx];

            if( time_in_queue < 0 ) // ie processor is free before arrival
                time_in_queue = 0;

            processing_time = time_in_queue + len[task_idx];

            if( processing_time <=10){
               if(processing_time < min_processing_time){
                    min_processing_time     = processing_time;
                    min_processing_time_idx = i;
               }
            }
        }

        if( min_processing_time_idx == -1){
            // No Existing CPU can solve this task.
            // Check if spawning a new CPU is possible.
            if (cpus_used_so_far+1 <= 5){
                //spawn a new CPU
                cpus_used_so_far++;
                int new_cpu_index = cpus_used_so_far - 1; // last CPU, converting to zero index

                exit_time_of_task_in_CPUs[new_cpu_index] = arr[task_idx] + len[task_idx] ;

            }else{
                cpus_used_so_far = -1; // not possible to spawn a new CPU,
                break;          //and you can't process with existing CPUs
            }

        }else{
            // Possible to handle with existing CPUs
            exit_time_of_task_in_CPUs[min_processing_time_idx] = arr[task_idx] + min_processing_time;
        }

    }


    cout << cpus_used_so_far <<endl;


}
  1. Trying memoization approach.

Recursive approach solves the problem, but it gets timed out. (No wonder)

What is the possible way to save the state of int, int, int[]?

I am also open to an iterative solution for the same.

CODE:

#include <iostream>
using namespace std;

#define MAXN 500

int min_cpus_used = 6;

int N;
int arr[MAXN];
int len[MAXN];

void recurse( int task_idx, int cpus_used_so_far, int  exit_time_of_task_in_CPUs[] ){

    if( task_idx == N-1){
        min_cpus_used = min( min_cpus_used, cpus_used_so_far);
        return ;
    }

    if( cpus_used_so_far >= min_cpus_used ){
        return ; //optimization
    }

    for(int i=0; i<cpus_used_so_far ; i++){

        int processing_time = 0;
        int time_in_queue = exit_time_of_task_in_CPUs[i] - arr[task_idx];

        if( time_in_queue < 0 ) // ie processor is free before arrival
            time_in_queue = 0;

        processing_time = time_in_queue + len[task_idx];

        // try with existing CPUs
        if( processing_time <=10){
            int prev =  exit_time_of_task_in_CPUs[i];
            exit_time_of_task_in_CPUs[i] = arr[task_idx] + processing_time;

            recurse( task_idx + 1 , cpus_used_so_far , exit_time_of_task_in_CPUs ); // can we optimize passing array

            exit_time_of_task_in_CPUs[i] = prev;

        }

        // try with new CPU
        if (cpus_used_so_far+1 <= 5){

            int new_cpu_index = cpus_used_so_far + 1 - 1; // converting to zero index

            int prev = exit_time_of_task_in_CPUs[new_cpu_index];

            exit_time_of_task_in_CPUs[new_cpu_index] = arr[task_idx] + len[task_idx] ;

            recurse( task_idx+1 , cpus_used_so_far+1 , exit_time_of_task_in_CPUs );

            exit_time_of_task_in_CPUs[new_cpu_index] = prev;

        }else{
            return ;
        }

    }

}



int main(){

    ios_base::sync_with_stdio(false);
    cin.tie(NULL);

    cin>>N;

    for(int i=0;i<N;i++){
        cin>> arr[i] >> len[i];
    }

    int exit_time_of_task_in_CPUs[5]={0};
    recurse(0, 1, exit_time_of_task_in_CPUs);

    if(min_cpus_used==6){
        cout<< -1 <<endl;
    }else{
        cout << min_cpus_used <<endl;
    }

}

Kindly help me with your thoughts on optimizing this solution.

PS: This has nothing to do with CPU Scheduling.

回答1:

Summary: bruteforce the answer (maximal number of CPUs), sort tasks by start time, assign them to CPUs in that order with DP with profile (profile is the nearest moment when a specific CPU is available; ignore order of CPUs).

The solution is rather sophisticated. Feel free to ask for clarifications.

Idea 1: let's brute force maximal number of CPUs and brute force it (or apply binary search: if A CPUs are enough, then A+1 CPUs are enough as well). Now we have a fixed number of CPUs and we have to solve a yes/no problem: is it possible to arrange tasks execution in such and such way? AS we have 5 CPUs max, that idea can be implemented with a simple outer for loop.

Idea 2: there is a solution which starts processing of any task at integer moment of time.

Proof: take any solution, choose the leftmost task for which that condition does not hold, and notice that one can move it further to the left. That reduces total sum of positions of all tasks. Now let's choose the solution which minimizes that value. It cannot have any tasks which start at non-integer times.

Idea 3: there is a solution with the following condition on each CPU: for every try tasks A and B such that A is executed right before B, A occurs in the sorted list of tasks earlier than B (sorted by their arrival/creation time).

Proof: take an arbitrary solution and bubble-sort tasks on each CPU. We have to prove that swapping two adjacent tasks does not make the solution incorrect. Consider tasks A and B such that A goes after B in the sorted list. That means that its creation time is not greater than B's creation time, so B exists at the time we started processing A. That also means that A's desired termination time is not greater than B's creation time, so at the time B is completed we're still allowed to complete A. So, swapping their order of execution does not make the solution incorrect and we're free to do so.

Idea 4: it's possible to build a solution by starting with empty execution plans for all CPUs and then appending tasks one-by-one to some plans, starting from the earliest arrived and finished with the latest arrived task. We append task to the lestmost possible time it can be started on a given CPU.

Proof: take an arbitrary solution with the property from idea 3. Note that execution plan of each CPU is a subsequence of the sorted list of tasks. Well, that means that each task in the sorted list can be assigned to a CPU, and after performing the algorithm from idea 4 we will end up with some execution plan. It'll be "no worse" than our original solution: positions of all tasks will be not greater than positions of tasks in the original plan. Therefore, that plan is also a solution.

Idea 5: run recursive backtracking to find distribution of tasks among CPUs. We already know order in which they should be added to CPUs (the order of creation). Keep track of first available time on each CPU and brute force assignment of the next task on each step of the search. E.g. for tasks of length 1, 5, 2, 9, 3 starting at zero, the assignment may be:

Task start:   0 0 0 0 0
Task length:  1 5 2 9 3
Assigned CPU: A B B A B

Idea 6: add memoization (aka convert backtracking to dynamic programming). Note that we don't really care if first available time on CPU is less than next task's starting time X, and it also can't be greater than X+10. So, we have 11 possibilities for each CPU, that's 11^5=161051 possibilities for 5 CPUs. That's the "profile" of our DP. Since we don't care about order of CPUs, answers for profiles which differ by permutation only are the same. So we can consider 3003 different profiles only (that's \choose{10+5}{5}). Another variable in our state is the number of the next task (500 possibilities), and we also have 5 transitions from each state (which CPU to use). That makes 3003*500*5 ~ 8m transitions, which is not a big number, so we can happily smash it under our outer for-loop from idea 1. Of course, it'd better to precalculate transitions between profiles, but it may also be possible to go without that.