Search⌘ K
AI Features

Activity Scheduling Problem

Explore the activity scheduling problem and understand how to select the maximum number of compatible activities using greedy algorithms. Learn to optimize solutions by sorting activities by finish time, reducing complexity, and applying an iterative approach suitable for coding interviews.

Problem Statement

Say you are given nn activities with their start and finish times. Select the maximum number of activities that can be performed by a single person, assuming that a person can only work on a single activity at a time.

Mathematically, Let SS == 1,2,...,n{1, 2, . . . , n} be the set of activities that compete for a resource. Each activity ii has its starting time sis_i and finish time fif_i with sis_i fif_i , namely, if selected, i takes place during time [sis_i , fif_i). No two activities can share the resource at any time point. We say that activities ii and jj are compatible if their time periods are disjoint.

The activity-selection problem is the problem of selecting the largest set of mutually compatible activities.

widget

Brute Force Solution

We always select the first activity, and then try all the combinations of the remaining tasks that can be performed with task 00 as the first task.

C++
// where
// n = Total number of activities
// start[] = start time of all activities
// finish[] = finish time of all activities
void maxActivities(int start[], int finish[], int n) {
int index, j;
std::vector < int > finalActivities;
std::vector < int > tempActivities;
cout << "Following activities are selected: 0 ";
// Array that will help us find all combinations possible of the activities
int combinations[n];
for (int i = 0; i < n; i++)
combinations[i] = i;
// find value for all combinations
do {
index = 0; //starting activity always 0
for (j = 1; j < n; j++) {
if (start[combinations[j]] >= finish[combinations[index]]) {
tempActivities.push_back(combinations[j]);
index = j;
}
}
if (tempActivities.size() > finalActivities.size())
finalActivities = tempActivities;
tempActivities.erase(tempActivities.begin(), tempActivities.end());
} while (std::next_permutation(combinations, combinations + n));
//printing the sequence with max tasks
for (int i = 0; i < finalActivities.size(); i++)
cout << finalActivities[i] << " ";
}
int main() {
int start[] = {0, 3, 3, 2, 4, 3, 8, 10};
int finish[] = {3, 4, 5, 7, 8, 11, 12, 13 };
int n = sizeof(start) / sizeof(start[0]);
maxActivities(start, finish, n);
return 0;
}

As you can see from the code above, such an approach is very naive. Plus the time complexity is O(2n)O(2^n) since we try all possible combinations of the activities.

However, a slight greedy tweak can really help us reduce the time complexity of our solution

By looking at the figure above, we can deduce that if we sort the activities on the final time of the activities, we can intuitively assign the resources that are free to the next task. Let’s have a look at the pseudocode for solving this algorithm:

Greedy Iterative Solution

Pseudocode

GreedyIterativeActivitySelector(A, s, f): 
    Sort A by finish times stored in f
    S = {A[1]} 
    k = 1
    n = A.length
    
    for i = 2 to n:
       if s[i] ≥ f[k]: 
           S = S U {A[i]}
           k = i
    
    return 

This algorithm is called Greedy-Iterative-Activity-Selector.

In this pseudocode, AA is an array containing the activities, ss is an array containing the start times of the activities in AA, and ff is an array containing the finish times of the activities in AA. Note, these arrays are indexed starting from 1 up to the length of the corresponding array.

Firstly, sort the array of activities in increasing order of finish times by using the finish times stored in the array ff. This operation can be done in O(nlog2n)O(n log_2 n) time, using the fastest available sorting algorithms. For revision of sorting algorithms see this chapter. Next, create a set SS to store the selected activities, and initialize it with the activity A[1]A[1]. Start iterating from the second element of that array AA up to its last element and, if the start time s[i]s[i] of the iith activity ( A[i]A[i]) is greater or equal to the finish time f[k]f[k] of the last selected activity (A[k]A[k]), then A[i]A[i] is compatible with the selected activities in the set SS, and thus it can be added to SS.

C++
bool sortbysec(const pair<int,int> &a, const pair<int,int> &b) {
return a.second < b.second;
}
void maxActivities(int start[], int finish[], int n) {
vector< pair <int,int> > zipped; //zipping start and finish arrays together
for(int i = 0; i < n; i++)
zipped.push_back(make_pair(start[i], finish[i]));
sort(zipped.begin(), zipped.end(), sortbysec);
int index, j;
cout << "Following activities are selected: ";
// select first activity
index = 0;
cout << index << " ";
for (j = 1; j < n; j++) {
if (zipped[j].first >= zipped[index].second) {
cout << j << " ";
index = j;
}
}
}
int main() {
int start[] = {0, 3, 3, 2, 4, 3, 8, 10};
int finish[] = {3, 4, 5, 7, 8, 11, 12, 13};
int n = sizeof(start) / sizeof(start[0]);
maxActivities(start, finish, n);
return 0;
}

The algorithm is O(n)O(n) (without sorting) because each activity is examined exactly once across all calls; each recursive call starts at mm, where the previous call left off.

If the activities need to be sorted, the overall problem can be solved in O(nlgn)O(nlgn).