Table of contents

Introduction to Greedy Algorithm

What is a Greedy Algorithm?

A Greedy Algorithm is an algorithmic approach that makes decisions step by step, selecting the best or most optimal choice at each step in the hope that this local optimization will lead to a globally optimal solution. The greedy approach builds a solution incrementally by always choosing the best available option at that moment, without considering the overall problem's future consequences.

Greedy algorithms are often used in optimization problems where the goal is to find a solution that maximizes or minimizes some value, such as profit, cost, or distance. However, greedy algorithms are not guaranteed to provide the optimal solution for all problems but work well for a specific set of problems called greedy-choice property problems.

How Greedy Algorithms Work ?

Greedy algorithms work by repeatedly selecting the locally optimal choice (the "greedy" choice) in each step with the hope that these local solutions will lead to the globally optimal solution. The key concept in greedy algorithms is that decisions are made based on the current situation without reconsidering previous decisions.

Steps of a Greedy Algorithm:

  1. Start: Begin with an empty solution.
  2. Make the Greedy Choice: At each step, make the best possible choice from the available options based on a certain criterion.
  3. Check Feasibility: Ensure that the choice is feasible (does not violate any problem constraints).
  4. Repeat: Repeat the process until a solution is constructed.
  5. Finish: When no more choices can be made, return the solution.

Characteristics of Greedy Algorithms

  1. Greedy Choice Property: A problem exhibits this property if a global optimal solution can be arrived at by selecting the local optimum (greedy choice) at each step.
  2. Optimal Substructure: A problem has an optimal substructure if the optimal solution to the problem contains optimal solutions to its subproblems.

Example of a Greedy Algorithm

Let’s consider the Activity Selection Problem as an example. The problem is to select the maximum number of activities that can be performed by a single person, given that each activity has a start and end time, and no two activities can overlap.

Steps of the Greedy Approach for the Activity Selection Problem:

  1. Sort activities by their finish times.
  2. Select the first activity (the one that finishes the earliest).
  3. For each subsequent activity, select it if its start time is after or at the finish time of the previously selected activity.
  4. Repeat this process until all activities have been considered.

Example of Greedy Algorithm: Activity Selection Problem

Given n activities with their start and finish times, select the maximum number of activities that can be performed by a single person. Each activity is represented as a pair (start, finish).

Example:

plaintext
Activities: [(1, 2), (3, 4), (0, 6), (5, 7), (8, 9), (5, 9)]

Steps:

  1. Sort the activities by their finish times: [(1, 2), (3, 4), (5, 7), (8, 9), (0, 6), (5, 9)].
  2. Select the activity (1, 2) (earliest finish time).
  3. The next activity is (3, 4), because its start time is after the finish time of (1, 2).
  4. Continue selecting activities based on their finish times.

Solution:

  • The selected activities are: [(1, 2), (3, 4), (5, 7), (8, 9)].

Here are examples of implementing the Activity Selection Problem using a greedy algorithm in C++, Java, and Python.

C++ Implementation of Greedy Algorithm

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

using namespace std;

// Function to select maximum number of activities
void activitySelection(vector<pair<int, int>>& activities) {
    // Sort activities based on their finish time
    sort(activities.begin(), activities.end(), [](pair<int, int> a, pair<int, int> b) {
        return a.second < b.second;
    });

    // Select the first activity
    cout << "Selected activities: ";
    int lastSelected = 0;
    cout << "(" << activities[lastSelected].first << ", " << activities[lastSelected].second << ") ";

    // Iterate through the sorted activities
    for (int i = 1; i < activities.size(); i++) {
        // Select the activity if it starts after the last selected activity finishes
        if (activities[i].first >= activities[lastSelected].second) {
            cout << "(" << activities[i].first << ", " << activities[i].second << ") ";
            lastSelected = i;
        }
    }
    cout << endl;
}

// Main function
int main() {
    vector<pair<int, int>> activities = {{1, 2}, {3, 4}, {0, 6}, {5, 7}, {8, 9}, {5, 9}};
    activitySelection(activities);
    return 0;
}

Java Implementation of Greedy Algorithm

java
import java.util.*;

public class ActivitySelection {
    // Function to select maximum number of activities
    public static void activitySelection(int[][] activities) {
        // Sort activities based on their finish time
        Arrays.sort(activities, Comparator.comparingInt(a -> a[1]));

        // Select the first activity
        System.out.print("Selected activities: ");
        int lastSelected = 0;
        System.out.print("(" + activities[lastSelected][0] + ", " + activities[lastSelected][1] + ") ");

        // Iterate through the sorted activities
        for (int i = 1; i < activities.length; i++) {
            // Select the activity if it starts after the last selected activity finishes
            if (activities[i][0] >= activities[lastSelected][1]) {
                System.out.print("(" + activities[i][0] + ", " + activities[i][1] + ") ");
                lastSelected = i;
            }
        }
        System.out.println();
    }

    // Main function
    public static void main(String[] args) {
        int[][] activities = {{1, 2}, {3, 4}, {0, 6}, {5, 7}, {8, 9}, {5, 9}};
        activitySelection(activities);
    }
}

Python Implementation of Greedy Algorithm

python
# Function to select maximum number of activities
def activity_selection(activities):
    # Sort activities based on their finish time
    activities.sort(key=lambda x: x[1])

    # Select the first activity
    print("Selected activities: ", end="")
    last_selected = 0
    print(f"({activities[last_selected][0]}, {activities[last_selected][1]})", end=" ")

    # Iterate through the sorted activities
    for i in range(1, len(activities)):
        # Select the activity if it starts after the last selected activity finishes
        if activities[i][0] >= activities[last_selected][1]:
            print(f"({activities[i][0]}, {activities[i][1]})", end=" ")
            last_selected = i
    print()

# Main function
if __name__ == "__main__":
    activities = [(1, 2), (3, 4), (0, 6), (5, 7), (8, 9), (5, 9)]
    activity_selection(activities)

When to Use Greedy Algorithms  ?

Greedy algorithms are used when:

  • The problem has both greedy choice property and optimal substructure.
  • The problem requires finding a solution that maximizes or minimizes some quantity, such as profit, cost, or distance.
  • The locally optimal choices always lead to a global optimum.

While greedy algorithms are fast and easy to implement, they may not always yield the optimal solution for problems that do not have the greedy-choice property. In such cases, dynamic programming or backtracking may be better approaches.

Properties of Greedy Algorithms

  1. Time Complexity: The time complexity of greedy algorithms depends on the problem. For the Activity Selection Problem, the time complexity is O(n log n) due to sorting.
  2. Space Complexity: Greedy algorithms usually have low space complexity. For most greedy algorithms, the space complexity is O(1), excluding the input space.

Advantages and Disadvantages of Greedy Algorithms

Advantages:

  1. Fast and Efficient: Greedy algorithms are generally faster because they make decisions without backtracking.
  2. Simple to Implement: Greedy algorithms are conceptually straightforward and easy to implement.
  3. Good for Specific Problems: Greedy algorithms provide optimal solutions for problems that exhibit the greedy-choice property, like Huffman coding, fractional knapsack, and minimum spanning tree.

Disadvantages:

  1. Not Always Optimal: For many problems, greedy algorithms fail to provide the globally optimal solution. They can only guarantee local optimization.
  2. May Require Additional Proof: Sometimes it’s hard to prove whether a greedy algorithm will always lead to the optimal solution.

Applications of Greedy Algorithms

  1. Activity Selection Problem: Select the maximum number of activities that don’t overlap.
  2. Huffman Coding: A greedy algorithm used in data compression to minimize the average code length of symbols.
  3. Fractional Knapsack Problem: Given weights and values of items, maximize the total value in a knapsack of limited capacity by selecting fractional quantities of items.
  4. Prim’s and Kruskal’s Algorithm: Used to find the Minimum Spanning Tree (MST) of a graph.
  5. Dijkstra’s Algorithm: Used to find the shortest path in a weighted graph from a single source node to all other nodes.
  6. Job Scheduling Problem: Schedule jobs to maximize profit, minimize time, or achieve other objectives based on certain constraints.

DSA

Related Articles