Code360 powered by Coding Ninjas X Naukri.com. Code360 powered by Coding Ninjas X Naukri.com
Table of contents
1.
Introduction
2.
Activity Selection Problem
3.
Greedy algorithms
4.
Activity selection using a greedy algorithm
5.
Brute Force Approach
5.1.
Dry Run 
5.2.
Algorithm for brute force approach
5.3.
Implementation using C++
5.4.
Complexity Analysis
6.
Optimal Approach
6.1.
Dry Run
6.2.
Algorithm for optimal approach
6.3.
Implementation using C++
6.4.
Complexity Analysis
6.5.
Implementation Using python
6.6.
Complexity Analysis
7.
Why will the greedy algorithm work for this problem?
8.
Frequently Asked Questions
8.1.
Describe the limitations of the greedy algorithm.
8.2.
Assuming the activities are sorted, what is the time complexity of the greedy algorithm-based solution for the activity selection problem?
8.3.
Is there a more optimal solution for the activity selection problem other than the one based on a greedy algorithm?
8.4.
Is Dijkstra a greedy algorithm? 
8.5.
Which is faster Greedy methods or dynamic programming methods?
9.
Conclusion
Last Updated: Mar 27, 2024

Activity Selection problem and Greedy Algorithm

gp-icon
Data structures & algorithms (Beginner to Intermediate)
Free guided path
13 chapters
99+ problems
gp-badge
Earn badges and level up

Introduction

The activity selection problem is an optimization problem that involves selecting a subset of mutually compatible activities from a given set of activities with specific start and finish times. The objective is to maximize the number of activities that can be completed while ensuring that no two activities overlap in time.

OG Image

In this blog, we will discuss the activity selection problem and we will look at the different approaches that can be used to solve this problem.

Activity Selection Problem

Suppose we have a processor that can process only one activity simultaneously. And we have some tasks to be completed on that processor. Now, each task has a start time and end time, so the task will be assigned to the processor at the start time. The processor can start its execution if it is not engaged. Else it will not start the execution of the task and will wait till the current task is executed. Once it is over, it can start executing the next available task. So this problem is called activity selection, where we have to find the maximum no of non-overlapping activities that can be completed from a given set of activities.

There could be several approaches to solve this problem. But here we will discuss the solution using a greedy algorithm, which selects the activity with the earliest finish time in each step.

Get the tech career you deserve, faster!
Connect with our expert counsellors to understand how to hack your way to success
User rating 4.7/5
1:1 doubt support
95% placement record
Akash Pal
Senior Software Engineer
326% Hike After Job Bootcamp
Himanshu Gusain
Programmer Analyst
32 LPA After Job Bootcamp
After Job
Bootcamp

Greedy algorithms

image for the greedy algorithm

Greedy algorithms make the best choice at each step to reach the overall optimal solution. So, while solving a problem with the greedy algorithm, we keep choosing the immediate best option. 

For example, if we have to get a change for 80 rupees in the minimum number of coins, we have an infinite supply of coins of 50, 20, and 10. Then, the greedy algorithm will suggest picking up the coin with the highest denomination and taking as many as possible, but since taking two coins of 50 will become more than 80, we will pick one coin of 50, then move to the next highest denomination, i.e., 20 after taking 20, we only require one coin of 10. So we will pick one coin of 10 and get the minimum number of coins for change of 80 as 3. 

image for the above example

We reached the optimal solution in the above example by choosing the best option at each stage. Thus any such problem can be solved using the greedy algorithm. Some examples of greedy algorithms are as follows- 

  • Dijkstra's Shortest Path Algorithm.
  • Kruskal's Minimum Spanning Tree.
  • Prim's Minimum Spanning Tree.
  • Reverse delete algorithm for MST.

Activity selection using a greedy algorithm

First, we will see how we can solve this problem using a min-heap (priority queue) if the activities given are in random order. After that, we will see an optimized method to solve this question without using a priority queue. 

One very important question is why we should always consider the first activity in the solution. The answer to this question lies in the problem statement itself. Since we have to find the maximum number of activities, the first activity will be the one that may or may not start late from some activities but will always finish first than any other activity. And choosing it will increase the no of activities completed to 1 in the least time. 

Brute Force Approach

In this approach, we will store the array of activities in the min heap of pairs. After storing them in the min heap of pairs, we will process the top element of the priority queue. The first top element would be our first activity with the least finish time. After that, we will store the finish time of this activity in a variable named end, and we will start processing the remaining elements of the priority queue. We will consider an activity only when the start time of the activity is greater than the finish time of the previously considered activity, which is stored in the variable end. We will process the elements till the priority queue becomes empty. 

Dry Run 

So now let's dry run a sample test case to understand the problem better, consider the array of activities given below in the image.

image of array before inserting elements in the prioirity queue

Now will sort the array, and the steps given in the image below will be performed.

dry run for approach using priority queue

Algorithm for brute force approach

1. Insert all the activities in a min priority queue of pairs. We will make pairs like this {finish_time, start_time} for an activity and then insert it in the priority queue.

2. Take the top element of the priority queue and pop it. This is our first activity which is completed.

3. Store the end time of this activity in a variable end and start popping elements of the priority queue one by one.

4. Now, if the activity on the top of the priority queue has a start time greater than the end time of the previously selected activity, then cout this activity and update the end variable with the value of the end time of the currently selected activity.

5. Repeat this till the priority queue is not empty.

Implementation using C++

// Program for activity selection for any random order using a priority queue.
#include <bits/stdc++.h>
using namespace std;

// Structure for activity.
struct activity{

    int start, end;
};

/*
	Function to select a set of maximum activities that can be executed.
	Given that only one activity can be executed at any instant.
*/ 
void solve(int n, vector<activity> actvty){

    priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> pq;
    int x, y;
    for (auto x: actvty){
    
        // Since we sort by end time so we will insert
        //activities in priority queue with a finish and start time
        pq.push({x.end, x.start});
    }

    cout << "The activities are selected in following order: ";
    
    // We will always select the activity with least finish time
    x = 0;

    auto current = pq.top();
    auto start = current.second;
    auto end = current.first;

    pq.pop();

    cout << "{" << start << "," << end << "} ";

    // To select from the rest of the activities.
    while (!pq.empty()){
    
        current = pq.top();
        pq.pop();

        /* 
        	An activity will be selected if it has a start time
        	greater than the end time of the previous activity.
        */ 
        if (current.second >= end){
        
            start = current.second;
            end = current.first;

            cout << "{" << start << "," << end << "} ";
        }
    }
}
// Driver function.
int main(){

    int n = 6;
    vector<activity> actvty = {{1, 2}, {4, 4}, {5, 10}, {9, 10}, {7, 8}, {2, 4}};
    solve(n, actvty);
}


Output

The activities are selected in following order: {1,2} {2,4} {4,4} {7,8} {9,10} 

Complexity Analysis

The time complexity of the above approach is- O(N*logN).

The space complexity of the above approach is- O(N).

Also Read - Selection Sort in C

Optimal Approach

Now we will see a more optimal approach, and here we will not use any extra array or Data Structure. Here we will sort the original array using a comparator function in the order of the activity's finish time. After that, we will process each activity just like the previous approach.

Dry Run

So now let's dry run a sample test case to understand the problem better, consider the array of activities given below in the image.

image of array before sorting

Now will sort the array, and the steps given in the image below will be performed.

image of dry run of optimal approach

Algorithm for optimal approach

1. Sort the activities in increasing order of their finish time.

2. Select the activity with the least finish time and execute it.

3. Now, we will select the next activity only when the start time of the current activity would be greater than the previously selected activity's finish time.

4. After selecting an activity, we will update the finish time by the current activity's finish time.

5. Repeat this till the array is completely traversed. 

Implementation using C++

// Program for activity selection for any random order
#include <bits/stdc++.h>
using namespace std;

// Structure for activity.

struct activity{

    int start, end;
};

// Comparator function for activities based on their end time.

bool compAct(activity a1, activity a2){

    return (a1.end < a2.end);
}

/*
	Function to select a set of maximum activities that can be executed.
	Given that only one activity can be executed at any instant.
*/ 

void solve(int n, vector<activity> actvty){

    int x, y;

    sort(actvty.begin(), actvty.end(), compAct);

    cout << "The activities are selected in following order." << endl;

    // As the activities are sorted we will always select the first activity.

    x = 0;

    cout << "{" << actvty[x].start << "," << actvty[x].end << "} ";

    // To select from the rest of the activities.

    for (y = 1; y < n; y++){

	/*
		An activity will be selected if it has a start time
		greater than the end time of the previous activity.       
	*/
	if (actvty[y].start >= actvty[x].end){
	
            cout << "{" << actvty[y].start << "," << actvty[y].end << "} ";

            x = y;
        }
    }
}

// Driver function.

int main(){

    int n = 6;

    vector<activity> actvty = {{1, 2}, {4, 4}, {5, 10}, {9, 10}, {7, 8}, {2, 4}};

    solve(n, actvty);

    return 0;
}

 

Output

The activities are selected in following order.
{1,2} {4,4} {7,8} {9,10} 

 

Complexity Analysis

The time complexity of the above approach is- O(N*logN).

The space complexity of the above approach is- O(1).

Implementation Using python

from typing import List, Tuple

# Structure for activity
Activity = Tuple[int, int]

# Comparator function for activities based on their end time
def comp_act(a1: Activity, a2: Activity) -> bool:
    return a1[1] < a2[1]

# Function to select a set of maximum activities that can be executed
# Given that only one activity can be executed at any instant
def solve(n: int, actvty: List[Activity]) -> None:
    x, y = 0, 0

    # Sort the activities by end time
    actvty.sort(key=lambda x: x[1])

    print("The activities are selected in the following order.")

    # As the activities are sorted we will always select the first activity
    print(f"{{{actvty[x][0]}, {actvty[x][1]}}}")

    # To select from the rest of the activities
    for y in range(1, n):
        # An activity will be selected if it has a start time
        # greater than the end time of the previous activity
        if actvty[y][0] >= actvty[x][1]:
            print(f"{{{actvty[y][0]}, {actvty[y][1]}}}")
            x = y

# Driver function
if __name__ == "__main__":
    n = 6
    actvty = [(1, 2), (4, 4), (5, 10), (9, 10), (7, 8), (2, 4)]
    solve(n, actvty)

 

Output

The activities are selected in the following order.
{1, 2}
{4, 4}
{7, 8}
{9, 10}

Complexity Analysis

The time complexity of the above approach is- O(N*logN).

The space complexity of the above approach is- O(1).

Check out this problem - Minimum Coin Change Problem 

Why will the greedy algorithm work for this problem?

A greedy algorithm works for the activity selection problem because of the following properties of the problem:

  1. The problem has the 'greedy-choice property', which means that the locally optimal choice (the activity with the earliest finish time) leads to a globally optimal solution.
     
  2. The problem has the 'optimal substructure' property, which means that an optimal solution to the problem can be constructed efficiently from optimal solutions to subproblems.
     

The greedy algorithm makes the locally optimal choice at each step by selecting the activity with the earliest finish time. This choice ensures that there is no overlap between the selected activities and that the maximum number of activities can be completed. Since the problem has the 'optimal substructure' property, a globally optimal solution can be constructed efficiently by making locally optimal choices at each step.

Frequently Asked Questions

Describe the limitations of the greedy algorithm.

The greedy algorithms can not consider future possibilities while making choices for solving a problem. It makes the best choice at each small step, which is sometimes not the optimal solution.

Assuming the activities are sorted, what is the time complexity of the greedy algorithm-based solution for the activity selection problem?

When the activities are sorted, the time complexity of the greedy algorithm-based solution for the activity selection problem will be O(N).

Is there a more optimal solution for the activity selection problem other than the one based on a greedy algorithm?

Even though the greedy algorithm has its limitations, the greedy algorithm-based solution for the activity selection problem is its optimal solution.

Is Dijkstra a greedy algorithm? 

Yes, Dijkstra is a greedy algorithm. It is used in a directed graph to solve the single-source shortest path problem.

Which is faster Greedy methods or dynamic programming methods?

Generally, we consider the greedy methods to be faster than dynamic programming methods because, for dynamic programming methods, we need to use the DP table, which will need extra memory.

Conclusion

In this blog, we learned about the activity selection problem and greedy algorithms-

We started by understanding the problem statement and how a greedy algorithm can solve these problems. We went on to learn a little bit about greedy algorithms, what these are and how they work. After that, we discussed two different approaches to solve this problem. The first one was brute force and the second one was the optimal approach. In the end, we concluded with the FAQs.

We hope this article has helped you in some way and if you liked our article, do upvote our article and help other ninjas grow.  You can refer to our Guided Path on Coding Ninjas Studio to upskill yourself in Data Structures and AlgorithmsCompetitive ProgrammingSystem Design, and many more!

Head over to our practice platform Coding Ninjas Studio to practice top problems, attempt mock tests, read interview experiences and interview bundles, follow guided paths for placement preparations, and much more!

Next article
Job scheduling algorithm
Guided path
Free
gridgp-icon
Data structures & algorithms (Beginner to Intermediate)
13 chapters
109+ Problems
gp-badge
Earn badges and level up
Live masterclass