top

Search

Software Key Tutorial

.

UpGrad

Software Key Tutorial

Activity selection problem

Introduction:

Welcome to the fascinating world of algorithmic problem-solving! In this comprehensive guide, we will explore the Activity Selection Problem, a classic optimization challenge frequently encountered in various scheduling and resource allocation tasks. Our journey will take us through the application of Greedy Algorithms techniques to solve this problem efficiently and effectively. Let’s dive straight into this intriguing topic!

Overview:

The Activity Selection Problem is a well-known optimization problem that involves selecting a maximum number of non-conflicting activities from a given set. Each activity has a start time and an end time, and the objective is to maximize the number of activities performed within the given time frame. We will explore two powerful techniques for solving this problem: Greedy Algorithms and Dynamic Programming.

Standard Greedy Algorithms:

Greedy Algorithms are widely used for optimization problems, including the Activity Selection Problem. Let's briefly explore some standard Greedy Algorithms to build a foundation for solving our target problem:

1. Kruskal's Minimum Spanning Tree (MST):

Kruskal's algorithm finds the minimum spanning tree in a weighted graph. Though not directly related to the Activity Selection Problem, understanding MST helps grasp the principles of Greedy Algorithms.

2. Prim's Minimum Spanning Tree:

Prim's algorithm also finds the minimum spanning tree, emphasizing a different approach. While not directly tied to activity selection, it reinforces the concept of greedy choices.

3. Dijkstra's Shortest Path:

Dijkstra's algorithm determines the shortest path from a source node to all other nodes in a weighted graph. While unrelated to the Activity Selection Problem, it illustrates the application of greedy principles.

4. Huffman Coding:

   Huffman coding is a data compression technique based on greedy choices. Although not directly linked to activity selection, it showcases the power of greedy algorithms in optimization problems.

How Does Greedy Choice Work For Activities Sorted According To Finish Time?

The essence of the Greedy Algorithm lies in making locally optimal choices to achieve a globally optimal solution. In the Activity Selection Problem, we sort the activities based on their finish times in ascending order. The greedy choice is to select the activity with the earliest finish time, as it leaves room for potential activities later. Let's understand this with an example:

Example:

Consider a set of activities (A, B, C, D) with their respective start and finish times:

A: (1, 3)

B: (2, 5)

C: (4, 7)

D: (6, 9)

The greedy approach would start by selecting activity A, as it has the earliest finish time. Now, A's finish time is 3, so it excludes any activities starting before time 3. Thus, the next selected activity would be C, and the final set of non-conflicting activities is {A, C}.

How To Implement When Given Activities Are Not Sorted?

When the activities are not initially sorted, we can still apply a Greedy Algorithm to solve the problem efficiently. Follow these steps:

1. Sort the activities based on their finish times in ascending order.

2. Initialize an empty set to store the selected activities.

3. Select the first activity with the earliest finish time.

4. Iterate through the sorted activities.

5. For each activity, if its start time is greater than or equal to the finish time of the last selected activity, add it to the set of selected activities.

Constraints Of Activity Selection Problem:

The Activity Selection Problem comes with specific constraints:

1. Non-negative integers serve as a representation of each activity's start and finish times.

2. The activities are given as a set of pairs, (start_time, finish_time).

3. The start and finish times are inclusive, i.e., the activity can be performed during the entire time interval [start_time, finish_time].

Approach 1: Greedy Algorithm:

The Greedy Algorithm's primary step is to sort the activities based on their finish times. Then, it iterates through the sorted list, selecting non-conflicting activities as discussed earlier. This approach provides an optimal solution for the Activity Selection Problem, with a time complexity of O(n log n), where n is the number of activities.

Example:

def select_activities_greedy(activities):
    activities.sort(key=lambda x: x[1])  # Sort by finish times
    selected_activities = [activities[0]]
    last_selected = 0

    for i in range(1, len(activities)):
        if activities[i][0] >= activities[last_selected][1]:
            selected_activities.append(activities[i])
            last_selected = i

    return selected_activities

# Example activities
activities = [(1, 3), (2, 5), (4, 7), (6, 9), (8, 11), (10, 13)]

# Using the Greedy Algorithm
selected_greedy = select_activities_greedy(activities)

# Output
print("Selected Activities using Greedy Algorithm:")
for activity in selected_greedy:
    print(activity)

Explanation:

In this code, the select_activities_greedy function uses the Greedy Algorithm to select the maximum number of non-conflicting activities. It sorts the activities by finish times, iterates through them, and selects activities that don't conflict with the previously selected ones. In the example provided, it prints the selected activities:

Output:

Selected Activities using Greedy Algorithm:

(1, 3)

(4, 7)

(8, 11)

Approach 2: Count the Maximum Number of Non-Conflicting Activities

Instead of listing selected actions, we compute the maximum number of non-conflicting tasks that can be done at once. The same principle applies: we sort activities by completion date in ascending order and repeat. If an operation's initial time exceeds the last selected operation's completion time, we increase the number of non-contradictory operations. The estimate represents the maximum at the end of the iteration, making it non-contradictory.

Example:

def count_activities(activities):
    activities.sort(key=lambda x: x[1])  # Sort by finish times
    count = 1
    last_selected = 0

    for i in range(1, len(activities)):
        if activities[i][0] >= activities[last_selected][1]:
            count += 1
            last_selected = i

    return count

# Example activities
activities = [(1, 3), (2, 5), (4, 7), (6, 9), (8, 11), (10, 13)]

# Count the Maximum Number of Non-Conflicting Activities
count = count_activities(activities)

# Output
print(f"Maximum Number of Non-Conflicting Activities: {count}")

Explanation:

In this code, the count_activities function counts the maximum number of non-conflicting activities without storing the activities themselves. It sorts the activities, counts them based on the Greedy Algorithm logic, and returns the count. In the example provided, it prints the count:

Output:

Maximum Number of Non-Conflicting Activities: 3

Approach 3: Print the Maximum Number of Non-Conflicting Activities

This approach is similar to Approach 2, but instead of counting the maximum number of non-conflicting activities, we directly print them as they are selected. As we iterate through the sorted activities, we print it out immediately whenever we find a non-conflicting activity.

Example:

def print_max_activities(activities):
    activities.sort(key=lambda x: x[1])  # Sort by finish times
    last_selected = 0
    selected_indices = [last_selected]

    for i in range(1, len(activities)):
        if activities[i][0] >= activities[last_selected][1]:
            selected_indices.append(i)
            last_selected = i

    print(f"Maximum Number of Non-Conflicting Activities: {len(selected_indices)}")
    print("Selected Activities:")
    for idx in selected_indices:
        print(activities[idx])

# Example activities
activities = [(1, 3), (2, 5), (4, 7), (6, 9), (8, 11), (10, 13)]

# Print the Maximum Number of Non-Conflicting Activities
print_max_activities(activities)

Explanation:

In this code, the print_max_activities function prints both the count and the selected activities without storing them in a separate list. It sorts the activities, keeps track of the selected indices, and prints the count along with the selected activities. In the example provided, it prints both the count and the selected activities:

Output:

Maximum Number of Non-Conflicting Activities: 3

Selected Activities:

(1, 3)

(4, 7)

(8, 11)

Dynamic Programming Approach

In the Dynamic Programming Activity Selection Problem solution, a table (dp array) maintains the maximum number of non-conflicting activities per index. Sorting by finish time and initialising dp[0] to 1 provides the subproblem base case. 

Next, we compare all past activities to determine the one with the longest finish time that doesn't clash with the present activity. The dp array stores the maximum number of non-conflicting activities up to the index. The set's maximum non-conflicting activities define dp. To get the best subset, we backtrack from dp[n-1] (where n is the number of activities) using dp values. Dynamic Programming easily integrates Activity Selection Problem subproblems for optimal solutions.

Activity selection problem in C++

The Activity Selection Problem in C++ includes choosing the most non-conflicting activities from a set. The start_time and finish_time of each activity are integers. The purpose is to discover the best subset of activities that can be done in the timeframe without conflicts.

Greedy Algorithms can efficiently answer the Activity Selection Problem. The Greedy Approach sorts activities by finish time and chooses the most non-conflicting ones.

Example:

```cpp
#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

struct Activity {
    int start_time;
    int finish_time;
};

bool sortByFinishTime(const Activity& a, const Activity& b) {
    return a.finish_time < b.finish_time;
}

vector<Activity> selectActivities(vector<Activity>& activities) {
    vector<Activity> selected_activities;
    sort(activities.begin(), activities.end(), sortByFinishTime);

    selected_activities.push_back(activities[0]);
    int last_selected = 0;

    for (int i = 1; i < activities.size(); i++) {
        if (activities[i].start_time >= activities[last_selected].finish_time) {
            selected_activities.push_back(activities[i]);
            last_selected = i;
        }
    }

    return selected_activities;
}

int main() {
    vector<Activity> activities = { {1, 3}, {2, 5}, {4, 7}, {6, 9} };
    vector<Activity> selected_activities = selectActivities(activities);

    cout << "Selected Activities: ";
    for (const Activity& activity : selected_activities) {
        cout << "(" << activity.start_time << ", " << activity.finish_time << ") ";
    }
    cout << endl;

    return 0;
}
```

Explanation:

In this C++ implementation, we define a struct `Activity` for each activity with start_time and finish_time characteristics. We then construct a custom comparator function `sortByFinishTime` to ascendingly sort the activities by finish time.

SelectActivities uses the Greedy Approach to select activities from a vector of activities. It arranges the activities by completion time, iterates through them, and selects the non-conflicting ones by comparing their start times to the last picked one.

The `main` function creates a vector of activities and calls the `selectActivities` function to get the best non-conflicting activities. Finally, we publish selected activities to the console.

This C++ code efficiently solves the Activity Selection Problem and shows how the Greedy Algorithm may optimize scheduling and resource allocation.

Time Complexity: 

The code arranges activities by completion dates using the std::sort function. Sorting takes O(n log n) time, where 'n' is the number of activities. This is because QuickSort, used by std::sort in C++, requires O(n log n) time to sort a vector of 'n' entries.

The function iterates through sorted activities once after categorising. This loop runs all 'n' activities once, making it O(n).

The most time-consuming part is sorting. Since it sorts, this algorithm takes O(n log n) time.

Space Complexity: 

The activities vector contains input activities, while the selected_activities vector contains chosen activities. These vectors have O(n) space complexity, where 'n' is the number of activities.

Other code variables like last_selected and loop indices use a fixed amount of space regardless of input size. Thus, they barely affect space complexity.

This algorithm has O(n) space and O(n log n) time complexity due to the sorting operation and two vectors that grow linearly with the number of activities.

Activity Selection Problem In Java:

In Java, we can represent each activity using a class or a data structure, such as an Activity class with start_time and finish_time attributes. We can use the Collections.sort method with a custom comparator to sort the activities based on finish times. The Greedy Algorithm can then be applied to select the maximum number of non-conflicting activities.

Example:

```java
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

class Activity {
    int start_time;
    int finish_time;

    public Activity(int start_time, int finish_time) {
        this.start_time = start_time;
        this.finish_time = finish_time;
    }
}

public class ActivitySelection {

    public static List<Activity> selectActivities(List<Activity> activities) {
        List<Activity> selectedActivities = new ArrayList<>();
        Collections.sort(activities, Comparator.comparingInt(a -> a.finish_time));

        selectedActivities.add(activities.get(0));
        int lastSelected = 0;

        for (int i = 1; i < activities.size(); i++) {
            if (activities.get(i).start_time >= activities.get(lastSelected).finish_time) {
                selectedActivities.add(activities.get(i));
                lastSelected = i;
            }
        }

        return selectedActivities;
    }

    public static void main(String[] args) {
        List<Activity> activities = new ArrayList<>();
        activities.add(new Activity(1, 3));
        activities.add(new Activity(2, 5));
        activities.add(new Activity(4, 7));
        activities.add(new Activity(6, 9));

        List<Activity> selectedActivities = selectActivities(activities);

        System.out.print("Selected Activities: ");
        for (Activity activity : selectedActivities) {
            System.out.print("(" + activity.start_time + ", " + activity.finish_time + ") ");
        }
        System.out.println();
    }
}
```

Explanation:

In Java, we define a "Activity" class with "start_time" and "finish_time" attributes for each activity. We then sort the activities by finish time using `Comparator.comparingInt`.

SelectActivities uses the Greedy Approach to select activities from a list. It arranges the activities by completion time, iterates through them, and selects the non-conflicting ones by comparing their start times to the last picked one.

The `main` method creates a list of activities and calls the `selectActivities` method to get the best non-conflicting subset. Finally, we publish selected activities to the console.

This Java version efficiently solves the Activity Selection Problem and shows how the Greedy Algorithm may be used in Java to optimize scheduling and resource allocation.

Activity Problem In Python:

We can represent each activity in Python as a tuple with (start_time, finish_time) elements. We can use the sorted function with a custom key to sort the activities based on finish times. The Greedy Algorithm can then be implemented to select the maximum number of non-conflicting activities.

Example:

def select_activities(activities):
    activities.sort(key=lambda x: x[1])
    selected_activities = [activities[0]]
    last_selected = 0

    for i in range(1, len(activities)):
        if activities[i][0] >= activities[last_selected][1]:
            selected_activities.append(activities[i])
            last_selected = i

    return selected_activities

activities = [(1, 3), (2, 5), (4, 7), (6, 9), (8, 11), (10, 13)]
selected_activities = select_activities(activities)

print("Selected Activities:", selected_activities)

Explanation:

In Python, select_activities takes a list of activities as input. `(start_time, finish_time)` represents each action. The Greedy Algorithm selects the most non-conflicting tasks.

The `select_activities` function sorts activities by end time using the `sort` method and a custom `key` function that retrieves the finish time from each activity tuple.

The function then initializes an empty list `selected_activities` to store the selected activities. It then adds the first activity with the earliest finish time to the specified activities.

From the second activity onward, the function loops. It compares each activity's start time to the last specified activity's finish time. If this criterion is met, the activity is non-conflicting and can be added to the specified list.

Finally, the function returns the list of selected activities, which is the maximum number of non-conflicting actions that can be done in the timeframe.

In the example provided, the `activities` list contains six activities:


After applying the `select_activities` function, the output will be:


These selected activities represent the maximum number of non-conflicting activities that can be performed within the given time frame, which is the desired solution to the Activity Selection Problem.

Conclusion:

In this tutorial, you've navigated the Activity Selection Problem and mastered the use of Greedy Algorithms and Dynamic Programming to solve it efficiently. With this information, you are now prepared to tackle a variety of optimization problems in computer science and beyond. Remember to carefully examine the problem restrictions and select the best technique for each circumstance. Good luck with your problem-solving!

FAQs:

1. Can Greedy Algorithms be applied to solve any optimization problem?

Greedy Algorithms are potent instruments for solving a variety of optimization problems, but they may not always provide the optimal solution for every issue. To assure the algorithm's efficacy, thorough analysis and proof of correctness are required.

2. What is the difference between Greedy Algorithms and Dynamic Programming?

Greedy Algorithms choose the best option at each step, whereas Dynamic Programming divides a problem into overlapping subproblems and caches their answers. Greedy Algorithms do not always produce the best global answer, whereas Dynamic Programming does by examining all options.

3. What is the time complexity of the Dynamic Programming approach for the Activity Selection Problem?

The time complexity of the Dynamic Programming approach is O(n^2), where n is the number of activities. The caching of subproblem solutions eliminates redundant computations and leads to an optimal solution.

Leave a Reply

Your email address will not be published. Required fields are marked *