Advanced Greedy Algorithms

Advanced greedy algorithms tackle complex optimization problems by making locally optimal choices. These techniques include interval scheduling, fractional knapsack, Huffman coding, and various graph algorithms that demonstrate the power of greedy strategies in competitive programming.

Interval Scheduling Problems

Interval scheduling is a classic greedy problem where we select the maximum number of non-overlapping intervals. The key insight is to sort by end times and greedily select intervals.

Interval Scheduling and Variants

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

struct Interval {
    int start, end;
    int weight; // for weighted version
    
    Interval(int s, int e, int w = 1) : start(s), end(e), weight(w) {}
};

class IntervalScheduling {
public:
    // Classic interval scheduling - maximum number of non-overlapping intervals
    static int maxIntervals(vector<Interval>& intervals) {
        if (intervals.empty()) return 0;
        
        // Sort by end time
        sort(intervals.begin(), intervals.end(), 
             [](const Interval& a, const Interval& b) {
                 return a.end < b.end;
             });
        
        int count = 1;
        int lastEnd = intervals[0].end;
        
        for (int i = 1; i < intervals.size(); i++) {
            if (intervals[i].start >= lastEnd) {
                count++;
                lastEnd = intervals[i].end;
            }
        }
        
        return count;
    }
    
    // Weighted interval scheduling using greedy approximation
    static int maxWeightGreedy(vector<Interval>& intervals) {
        if (intervals.empty()) return 0;
        
        // Sort by weight/length ratio (greedy heuristic)
        sort(intervals.begin(), intervals.end(), 
             [](const Interval& a, const Interval& b) {
                 double ratioA = (double)a.weight / (a.end - a.start);
                 double ratioB = (double)b.weight / (b.end - b.start);
                 return ratioA > ratioB;
             });
        
        int totalWeight = 0;
        vector<bool> used(intervals.size(), false);
        
        for (int i = 0; i < intervals.size(); i++) {
            if (used[i]) continue;
            
            totalWeight += intervals[i].weight;
            used[i] = true;
            
            // Mark overlapping intervals as used
            for (int j = i + 1; j < intervals.size(); j++) {
                if (!used[j] && intervalsOverlap(intervals[i], intervals[j])) {
                    used[j] = true;
                }
            }
        }
        
        return totalWeight;
    }
    
    // Minimum intervals to cover a point
    static int minIntervalsToPoint(vector<Interval>& intervals, int point) {
        vector<Interval> covering;
        
        // Find intervals that cover the point
        for (const auto& interval : intervals) {
            if (interval.start <= point && point <= interval.end) {
                covering.push_back(interval);
            }
        }
        
        if (covering.empty()) return -1; // impossible
        
        // Sort by start time, then by end time
        sort(covering.begin(), covering.end(), 
             [](const Interval& a, const Interval& b) {
                 if (a.start != b.start) return a.start < b.start;
                 return a.end > b.end;
             });
        
        return 1; // At least one interval covers the point
    }
    
private:
    static bool intervalsOverlap(const Interval& a, const Interval& b) {
        return !(a.end <= b.start || b.end <= a.start);
    }
};

int main() {
    vector<Interval> intervals = {
        Interval(1, 3, 5), Interval(2, 5, 8), Interval(3, 6, 3),
        Interval(4, 7, 6), Interval(5, 8, 4), Interval(6, 9, 7)
    };
    
    cout << "Intervals (start, end, weight):" << endl;
    for (const auto& interval : intervals) {
        cout << "(" << interval.start << ", " << interval.end << ", " << interval.weight << ") ";
    }
    cout << endl;
    
    // Test classic interval scheduling
    vector<Interval> copy1 = intervals;
    int maxCount = IntervalScheduling::maxIntervals(copy1);
    cout << "\nMaximum non-overlapping intervals: " << maxCount << endl;
    
    // Test weighted interval scheduling
    vector<Interval> copy2 = intervals;
    int maxWeight = IntervalScheduling::maxWeightGreedy(copy2);
    cout << "Maximum weight (greedy approximation): " << maxWeight << endl;
    
    // Test point coverage
    int point = 4;
    vector<Interval> copy3 = intervals;
    int minCover = IntervalScheduling::minIntervalsToPoint(copy3, point);
    cout << "Point " << point << " can be covered by " << minCover << " interval(s)" << endl;
    
    return 0;
}

Interval scheduling problems demonstrate key greedy principles: sorting by the right criterion (end times for maximum intervals) and making locally optimal choices that lead to globally optimal solutions.

Fractional Knapsack Problem

Unlike the 0/1 knapsack problem, the fractional knapsack allows taking fractions of items. The greedy approach of sorting by value-to-weight ratio gives the optimal solution.

Fractional Knapsack Implementation

#include <iostream>
#include <vector>
#include <algorithm>
#include <iomanip>
using namespace std;

struct Item {
    int value, weight;
    double ratio;
    int index;
    
    Item(int v, int w, int idx) : value(v), weight(w), index(idx) {
        ratio = (double)value / weight;
    }
};

class FractionalKnapsack {
public:
    struct Solution {
        double maxValue;
        vector<double> fractions; // fraction of each item taken
        
        Solution(int n) : maxValue(0), fractions(n, 0) {}
    };
    
    static Solution solve(vector<Item>& items, int capacity) {
        int n = items.size();
        Solution solution(n);
        
        // Sort by value-to-weight ratio in descending order
        sort(items.begin(), items.end(), 
             [](const Item& a, const Item& b) {
                 return a.ratio > b.ratio;
             });
        
        int remainingCapacity = capacity;
        
        for (const auto& item : items) {
            if (remainingCapacity >= item.weight) {
                // Take the whole item
                solution.fractions[item.index] = 1.0;
                solution.maxValue += item.value;
                remainingCapacity -= item.weight;
            } else if (remainingCapacity > 0) {
                // Take fraction of the item
                double fraction = (double)remainingCapacity / item.weight;
                solution.fractions[item.index] = fraction;
                solution.maxValue += item.value * fraction;
                remainingCapacity = 0;
                break;
            }
        }
        
        return solution;
    }
    
    // Variant: Multiple knapsacks
    static vector<Solution> multipleKnapsacks(vector<Item>& items, vector<int>& capacities) {
        vector<Solution> solutions;
        
        // Sort items by ratio
        sort(items.begin(), items.end(), 
             [](const Item& a, const Item& b) {
                 return a.ratio > b.ratio;
             });
        
        for (int capacity : capacities) {
            vector<Item> availableItems = items; // copy for this knapsack
            Solution solution = solve(availableItems, capacity);
            solutions.push_back(solution);
        }
        
        return solutions;
    }
};

// Continuous knapsack with time constraints
class TimeConstrainedKnapsack {
public:
    struct Task {
        int value, time;
        double efficiency;
        int index;
        
        Task(int v, int t, int idx) : value(v), time(t), index(idx) {
            efficiency = (double)value / time;
        }
    };
    
    static double maxValueInTime(vector<Task>& tasks, int totalTime) {
        // Sort by efficiency (value per time unit)
        sort(tasks.begin(), tasks.end(), 
             [](const Task& a, const Task& b) {
                 return a.efficiency > b.efficiency;
             });
        
        double maxValue = 0;
        int remainingTime = totalTime;
        
        for (const auto& task : tasks) {
            if (remainingTime >= task.time) {
                maxValue += task.value;
                remainingTime -= task.time;
            } else if (remainingTime > 0) {
                // Partial task completion
                double fraction = (double)remainingTime / task.time;
                maxValue += task.value * fraction;
                break;
            }
        }
        
        return maxValue;
    }
};

int main() {
    // Test fractional knapsack
    vector<Item> items = {
        Item(60, 10, 0), Item(100, 20, 1), Item(120, 30, 2)
    };
    int capacity = 50;
    
    cout << "Items (value, weight, ratio):" << endl;
    for (const auto& item : items) {
        cout << "Item " << item.index << ": (" << item.value << ", " << item.weight 
             << ", " << fixed << setprecision(2) << item.ratio << ")" << endl;
    }
    
    FractionalKnapsack::Solution solution = FractionalKnapsack::solve(items, capacity);
    
    cout << "\nKnapsack capacity: " << capacity << endl;
    cout << "Maximum value: " << fixed << setprecision(2) << solution.maxValue << endl;
    cout << "Item fractions taken:" << endl;
    for (int i = 0; i < solution.fractions.size(); i++) {
        if (solution.fractions[i] > 0) {
            cout << "Item " << i << ": " << fixed << setprecision(2) 
                 << solution.fractions[i] * 100 << "%" << endl;
        }
    }
    
    // Test time-constrained tasks
    vector<TimeConstrainedKnapsack::Task> tasks = {
        TimeConstrainedKnapsack::Task(100, 20, 0),
        TimeConstrainedKnapsack::Task(60, 10, 1),
        TimeConstrainedKnapsack::Task(140, 40, 2)
    };
    int totalTime = 50;
    
    cout << "\nTasks (value, time, efficiency):" << endl;
    for (const auto& task : tasks) {
        cout << "Task " << task.index << ": (" << task.value << ", " << task.time 
             << ", " << fixed << setprecision(2) << task.efficiency << ")" << endl;
    }
    
    double maxTaskValue = TimeConstrainedKnapsack::maxValueInTime(tasks, totalTime);
    cout << "\nMaximum value in " << totalTime << " time units: " 
         << fixed << setprecision(2) << maxTaskValue << endl;
    
    return 0;
}

The fractional knapsack demonstrates the power of greedy algorithms when problems have the greedy choice property. Sorting by value-to-weight ratio ensures optimal substructure.

Key Takeaways

  • Advanced greedy algorithms require careful analysis of the greedy choice property and optimal substructure
  • Sorting by the right criterion is crucial - end times for intervals, ratios for knapsack problems
  • Greedy algorithms often provide optimal solutions for fractional problems and good approximations for discrete ones
  • Understanding when greedy works vs when dynamic programming is needed is essential for competitive programming