Unit - 3
Greedy Algorithms
1. Introduction
1.1 Overview
- A greedy algorithm always makes the choice that seems to be the best at that moment.
- It never reconsiders this decision once it has been made.
- Example: Getting the best possible dress from a shop for yourself.
2. Characteristics of Greedy Algorithms
2.1 Key Features
- The greedy approach forms a set or list of candidates .
- Once a candidate is selected in the solution, it is there forever. Once a candidate is excluded from the solution, it is never reconsidered.
- To construct the solution in an optimal way, the Greedy Algorithm maintains two sets:
- One set contains candidates that have already been considered and chosen.
- The other set contains candidates that have been considered but rejected.
3. Elements of Greedy Strategy
3.1 Four Functions
- Solution Function: Checks whether a chosen set of items provides a solution.
- Feasible Function: Checks the feasibility of a set.
- Selection Function: Tells which of the candidates is the most promising.
- Objective Function: Gives the value of a solution (doesn't always appear explicitly).
4. Minimum Spanning Tree (MST)
4.1 Concept
- Let be a connected, undirected graph where:
- is the set of nodes.
- is the set of edges.
- Each edge has a given positive length or weight.
- A spanning tree of a graph is a sub-graph which is a tree and contains all the vertices of but does not contain cycles.
- A minimum spanning tree (MST) of a weighted connected graph is a spanning tree with the minimum or smallest weight of edges.
4.2 Algorithms for MST
- Kruskal’s Algorithm
- Prim’s Algorithm
5. Kruskal’s Algorithm
5.1 Concept
- A greedy algorithm that finds an MST for a connected weighted graph by adding increasing cost edges at each step.
5.2 Step-by-Step Explanation
- Sort: Sort all edges of the graph in increasing order of their weight.
- Initialize: Create a disjoint set for each vertex to keep track of connected components. Create an empty set
Tfor the MST. - Iterate: Loop through the sorted edges. For each edge :
- Check if and belong to different connected components using
find(u)andfind(v). - If they do, adding this edge won't form a cycle. Add it to
Tandmerge(u, v)the two components. - If they belong to the same component, discard the edge (as it would form a cycle).
- Check if and belong to different connected components using
- Terminate: Stop when
Tcontains edges (where is the number of vertices).
5.3 Algorithm Complexity
- Time Complexity: or where is the number of edges and is the number of vertices (due to sorting edges).
- Space Complexity: for the disjoint set data structure.
5.4 Algorithm
- Simple (Easy to Remember)
- Formal Algorithm (Code)
💡 The Big Idea in Plain English
Imagine connecting cities with roads at the lowest possible cost: You list every possible road sorted from cheapest to most expensive. You start building from the cheapest road. If a road would create a redundant loop (cycle) between cities that can already reach each other, you simply skip it! Once every city is connected without any loops, you're done!
👣 3 Easy Steps to Remember
- Sort All Edges: List all edges in ascending order of their weights (cheapest first).
- Pick the Cheapest Edge:
- Check if the two endpoints and are already in the same connected group (
FindSet(u) == FindSet(v)). - If different groups (No Cycle): Include this edge in the MST and merge their groups (
Union(u, v)). - If same group (Forms a Cycle): Discard the edge!
- Check if the two endpoints and are already in the same connected group (
- Finish: Stop when you have collected exactly edges (connecting all vertices).
🧠 Memory Trick: Sort edges Pick cheapest Skip cycles (Union-Find). Edge-focused approach with time complexity.
Algorithm Kruskal(G, w)
// Finds Minimum Spanning Tree (MST) using greedy approach
// Input: Connected, undirected weighted graph G = (V, E) with weight function w
// Output: Set of edges A forming the MST
{
A <- empty set;
for each vertex v in V do
MakeSet(v);
// Sort edges into non-decreasing order by weight w
sort the edges of E in non-decreasing order of weight w;
for each edge (u, v) in E (taken in sorted order) do
{
if (FindSet(u) != FindSet(v)) then
{
A <- A union {(u, v)};
Union(u, v);
}
}
return A;
}
6. Prim’s Algorithm
6.1 Concept
- In Prim's algorithm, the minimum spanning tree grows in a natural way, starting from an arbitrary root.
- At each stage, a new branch is added to the tree already constructed; the algorithm stops when all nodes have been reached.
6.2 Step-by-Step Explanation
- Initialize: Create a set to keep track of vertices included in the MST.
- Start: Pick an arbitrary vertex and add it to .
- Iterate: While does not contain all vertices:
- Find the edge with the minimum weight such that and .
- Add to and include in the MST.
- Terminate: Return the MST when all vertices are in .
6.3 Algorithm Complexity
- Time Complexity: with an adjacency matrix, or using a Min-Heap.
- Space Complexity: or depending on graph representation.
6.4 Algorithm
- Simple (Easy to Remember)
- Formal Algorithm (Code)
💡 The Big Idea in Plain English
Think of laying electrical cables outwards from your house: You start at any single house (the root). You look at all the neighboring houses directly reachable from your current wired network and ask: "Which neighbor is the cheapest to connect?" You lay cable to that cheapest neighbor. Now your wired network has two houses. Repeat: always pick the cheapest edge connecting an already-wired house to an un-wired house. The tree grows steadily outward like a creeping vine until all houses are powered!
👣 3 Easy Steps to Remember
- Plant the Seed: Pick any start node . Set its cost to (
key[r] = 0) and all other nodes to . Put all nodes in a priority queue. - Pick the Cheapest Unreached Node:
- Pull the node with the lowest cost out of the queue (
ExtractMin). It is now locked into the MST!
- Pull the node with the lowest cost out of the queue (
- Update Its Neighbors:
- Look at all neighbors of that are still waiting in the queue.
- If the road from to is cheaper than 's current best known connection, update 's cost and remember that connects through (
parent[v] = u). - Repeat until the queue is empty!
🧠 Memory Trick: Start with 1 node Always grow outward along the cheapest attached branch All nodes covered! Vertex-focused approach with time using a Min-Heap.
Algorithm Prim(G, w, r)
// Finds Minimum Spanning Tree (MST) starting from root vertex r
// Input: Connected, undirected weighted graph G = (V, E) with weight function w, root r
// Output: MST represented by parent pointers parent[u]
{
for each u in V do
{
key[u] <- infinity;
parent[u] <- NIL;
}
key[r] <- 0;
Q <- V; // Build min-priority queue of all vertices keyed by key[u]
while not EMPTY(Q) do
{
u <- ExtractMin(Q);
for each vertex v in Adj[u] do
{
if (v in Q and w(u, v) < key[v]) then
{
parent[v] <- u;
key[v] <- w(u, v);
DecreaseKey(Q, v, key[v]);
}
}
}
}
7. Dijkstra’s Algorithm
7.1 Concept
- Used to find the Single Source Shortest Path in a directed (or undirected) graph with positive edge weights.
7.2 Step-by-Step Explanation
- Initialize: Create a distance array initialized to infinity () for all vertices except the source vertex, which is set to .
- Set up: Maintain a set of unvisited vertices .
- Iterate:
- Extract the vertex from that has the minimum distance value .
- For every adjacent vertex of :
- If , update .
- Terminate: The algorithm finishes when all vertices are evaluated. The array will hold the shortest distances from the source to all other vertices.
7.3 Algorithm Complexity
- Time Complexity: with basic arrays, or using a Min-Priority Queue.
- Space Complexity: for the distance array and priority queue.
7.4 Algorithm
- Simple (Easy to Remember)
- Formal Algorithm (Code)
💡 The Big Idea in Plain English
Think of GPS navigation calculating the quickest route from your home to everywhere else: You start at home with distance , while the distance to every other location is set to (unexplored). At each step, you pick the unvisited location currently closest to home. From there, you check each road to its neighbors and ask: "Can I get to neighbor faster by driving through here?" If the shortcut is better, you update 's shortest distance (this is called edge relaxation!). Repeat until every location has been visited.
👣 3 Easy Steps to Remember
- Initialize Distances:
- Distance to source is (
dist[s] = 0). - Distance to all other nodes is (
dist[v] = infinity). - Add all vertices into a Min-Priority Queue.
- Distance to source is (
- Pick the Closest Node:
- Extract the node with the minimum
dist[u]. Its shortest path from source is now finalized!
- Extract the node with the minimum
- Relax Neighbors:
- For every neighbor of :
- If taking the road through is shorter:
dist[u] + weight(u, v) < dist[v] - Update:
dist[v] = dist[u] + weight(u, v)and setparent[v] = u. - Repeat until the queue is empty!
🧠 Memory Trick: Start at 0, others Pick smallest Relax neighbors: if
dist[u] + cost < dist[v], take the shortcut! Works only for non-negative edge weights. Runs in with a Min-Heap.
Algorithm Dijkstra(G, w, s)
// Single-Source Shortest Paths algorithm on weighted graph with non-negative weights
// Input: Graph G = (V, E), non-negative weight function w, source vertex s
// Output: Array dist[v] containing shortest path distance from s to each vertex v in V
{
for each vertex v in V do
{
dist[v] <- infinity;
parent[v] <- NIL;
}
dist[s] <- 0;
S <- empty set;
Q <- V; // Priority queue of all vertices keyed by dist[v]
while not EMPTY(Q) do
{
u <- ExtractMin(Q);
S <- S union {u};
for each vertex v in Adj[u] do
{
// Edge relaxation step
if (dist[u] + w(u, v) < dist[v]) then
{
dist[v] <- dist[u] + w(u, v);
parent[v] <- u;
DecreaseKey(Q, v, dist[v]);
}
}
}
}
8. Fractional Knapsack Problem
8.1 Concept
- Given objects and a knapsack capacity .
- Object has a positive weight and a positive value .
- Goal: Fill the knapsack to maximize the value, breaking objects into fractions if necessary ().
8.2 Step-by-Step Explanation
- Calculate Ratio: Compute the value-to-weight ratio for each item.
- Sort: Sort all items in descending order based on their ratio.
- Select: Iterate through the sorted items:
- If the item can completely fit in the remaining knapsack capacity, add it fully.
- If the item cannot fully fit, add the fraction of the item that exactly fills the remaining capacity, then stop.
- Return: The total accumulated value.
8.3 Algorithm Complexity
- Time Complexity: due to sorting.
- Space Complexity: if sorted in place, otherwise .
8.4 Algorithm
- Simple (Easy to Remember)
- Formal Algorithm (Code)
💡 The Big Idea in Plain English
Imagine filling a backpack with spices or gold dust that can be divided into any amount: You want the maximum possible total value, but your bag can only carry kilograms. The smartest greedy strategy is simple: calculate the price per kilogram for each item! Always take the item with the highest price per kg first. When your bag is almost full and the next item is too heavy to fit completely, chop it and take just the fraction that fills the remaining space!
👣 3 Easy Steps to Remember
- Calculate Value-per-Weight: For each item , compute its ratio: .
- Sort Items: Arrange all items in descending order of ratio (best value per kg at the top).
- Fill the Knapsack:
- For each item, if its entire weight fits into the remaining capacity: take all of it () and reduce remaining capacity.
- If it cannot fit completely: take the exact fraction that tops off the bag () and stop!
🧠 Memory Trick: Calculate Profit/Weight Sort highest first Take whole items Slice the last item to fill the gap! Runs in time due to sorting.
Algorithm FractionalKnapsack(W, n, p, w)
// Solves the Fractional Knapsack problem using a greedy approach
// Input: Knapsack capacity W, number of items n, profits p[1..n], weights w[1..n]
// Output: Array x[1..n] where x[i] is the fraction of item i included (0 <= x[i] <= 1)
{
// Sort items in non-increasing order of value-to-weight ratio p[i]/w[i]
sort items such that p[1]/w[1] >= p[2]/w[2] >= ... >= p[n]/w[n];
for i <- 1 to n do
x[i] <- 0.0;
remainingCapacity <- W;
for i <- 1 to n do
{
if (w[i] <= remainingCapacity) then
{
x[i] <- 1.0;
remainingCapacity <- remainingCapacity - w[i];
}
else
{
// Take fractional share of item i
x[i] <- remainingCapacity / w[i];
remainingCapacity <- 0;
break;
}
}
return x;
}
9. Activity Selection Problem
9.1 Concept
- Given a set of activities with a start time and finish time .
- Find the maximum size set of mutually compatible activities (activities that do not overlap).
9.2 Step-by-Step Explanation
- Sort: Sort all input activities in increasing order of their finish times ().
- Select First: Always select the first activity from the sorted array, as it finishes earliest and leaves maximum time for subsequent activities.
- Iterate: For each remaining activity:
- If its start time is greater than or equal to the finish time of the previously selected activity, add it to the selected set.
- Return: The set of selected activities.
9.3 Algorithm Complexity
- Time Complexity: due to sorting. If already sorted, it takes .
- Space Complexity: or depending on storage for results.
9.4 Algorithm
- Simple (Easy to Remember)
- Formal Algorithm (Code)
💡 The Big Idea in Plain English
Imagine you have one single conference hall and 10 speakers wanting to use it today: Each speaker gives you their start time and end time. You want to host the maximum number of talks without any overlaps. What is the golden rule? Always pick the talk that finishes earliest! Why? Because finishing early leaves the most free time remaining for future speakers throughout the rest of the day!
👣 3 Easy Steps to Remember
- Sort by Finish Time: Arrange all activities in order of when they finish ().
- Pick the Earliest Finisher: Always schedule the very first activity (). Set your clock pointer .
- Filter Compatible Talks:
- Walk through the remaining activities one by one ().
- If activity starts after or when activity finishes ():
- Book it! Add to your schedule ().
- Update your clock: .
- Otherwise, skip it because it clashes!
🧠 Memory Trick: Sort by finish time Take the first Take next whenever
start >= last_finish! Greedy choice: whoever ends earliest wins! Runs in time.
Algorithm ActivitySelection(s, f, n)
// Selects a maximum-size set of mutually compatible activities
// Input: Start times s[1..n] and finish times f[1..n] sorted such that f[1] <= f[2] <= ... <= f[n]
// Output: Set A of selected mutually compatible activities
{
A <- {1}; // Select the first activity
k <- 1; // Index of the last activity selected
for m <- 2 to n do
{
// If start time of activity m is >= finish time of last selected activity
if (s[m] >= f[k]) then
{
A <- A union {m};
k <- m;
}
}
return A;
}
10. Huffman Codes
10.1 Concept
- A greedy algorithm that constructs an optimal prefix code for lossless data compression.
- It assigns variable-length codes to input characters based on their frequencies.
- The most frequent character gets the smallest code, while the least frequent character gets the largest code.
- Prefix codes ensure that no code is a prefix of another code, preventing decoding ambiguity.
10.2 Step-by-Step Explanation
- Count Frequencies: Calculate the frequency of each character in the input data.
- Initialize Min-Heap: Create a leaf node for each character and insert them into a priority queue (min-heap) based on frequency.
- Build Tree: While the heap contains more than one node:
- Extract the two nodes with the minimum frequencies.
- Create a new internal node with a frequency equal to the sum of the two nodes.
- Attach the two nodes as left and right children.
- Insert the new node back into the min-heap.
- Assign Codes: Traverse the tree from the root. Assign
0to the left branch and1to the right branch. The path from the root to a leaf node provides the Huffman code for that character.
10.3 Algorithm Complexity
- Time Complexity: to build the heap and tree.
- Space Complexity: for storing the tree and codes.
10.4 Algorithm
- Simple (Easy to Remember)
- Formal Algorithm (Code)
💡 The Big Idea in Plain English
Think of Morse code or ZIP file compression:
In any English book, the letter 'E' appears thousands of times, while 'Z' is very rare.
If we assign 'E' a super short code (like 0) and 'Z' a longer code (like 11101), the entire book becomes much smaller!
Huffman coding creates the perfect variable-length code:
It repeatedly merges the two rarest characters together, building a binary tree from the ground up until every character has a code!
👣 3 Easy Steps to Remember
- Count & Put into Min-Heap: Count how often each character appears, and place all characters into a Min-Priority Queue.
- Merge the Two Smallest Nodes:
- While the queue has more than 1 node:
- Pop the two nodes with the smallest frequencies ( and ).
- Create a parent node whose frequency is their sum ().
- Set as the left child and as the right child.
- Put back into the queue!
- Assign Binary Bits:
- The final remaining node is the root of the tree.
- Trace from the root to every leaf: label left branches with
0and right branches with1. - The sequence of bits leading to each character is its compressed prefix code!
🧠 Memory Trick: Take 2 lowest frequencies Merge into parent Push back into Min-Heap Repeat until 1 tree Left=0, Right=1. Frequent letters get short codes! Runs in time.
Algorithm Huffman(C)
// Generates optimal prefix codes (Huffman tree) for characters in set C
// Input: Set C of characters, where each c in C has frequency c.freq
// Output: Root node of the optimal prefix tree
{
n <- |C|;
Q <- C; // Initialize min-priority queue with all characters keyed by frequency
for i <- 1 to n - 1 do
{
allocate new internal node z;
z.left <- x <- ExtractMin(Q);
z.right <- y <- ExtractMin(Q);
z.freq <- x.freq + y.freq;
Insert(Q, z);
}
return ExtractMin(Q); // Returns root of Huffman tree
}