Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Greedy Algorithm Principles and Problem Solving Patterns

Tech May 9 4

Core Concepts

The fundamental idea behind greedy algorithms is making the locally optimal choice at each stage with the hope of finding a global optimum. For instance, if given a stack of banknotes and allowed to pick ten, selecting the highest denomination each time ensures the maximum total value. This local maximization leads to the global solution.

However, this approach does not work for all problems. Consider packing a backpack with a volume limit; choosing the largest items first might prevent fitting more smaller items that yield a better total value. Such scenarios typically require dynamic programming.

Strategy Identification

There is no fixed template for greedy algorithms. The challenge lies in proving that local optimal choices lead to a global optimum. This often requires manual simulation or mathematical proof. If simulation suggests the strategy works, its worth implementing; otherwise, dynamic programming might be necessary.

Implementation Workflow

  1. Decompose the problem into smaller sub-problems.
  2. Identify a suitable greedy strategy.
  3. Solve each sub-problem optimally.
  4. Combine local solutions to form the global result.

In practice, strictly following these steps is less common than simply identifying the local optimal condition and verifying if it yields the global optimum.

Common Problem Patterns

Assign Cookies

Strategy: Satisfy children with smaller appetites first using the smallest available cookies.

class Solution {
public:
    int findContentChildren(vector<int>& appetites, vector<int>& cookieSizes) {
        std::sort(appetites.begin(), appetites.end());
        std::sort(cookieSizes.begin(), cookieSizes.end());
        
        int satisfiedCount = 0;
        for (int cookieIdx = 0; cookieIdx < cookieSizes.size() && satisfiedCount < appetites.size(); ++cookieIdx) {
            if (cookieSizes[cookieIdx] >= appetites[satisfiedCount]) {
                satisfiedCount++;
            }
        }
        return satisfiedCount;
    }
};

Wiggle Subsequence

Strategy: Count peaks and valleys by tracking the difference between consecutive elements.

class Solution {
public:
    int wiggleMaxLength(vector<int>& nums) {
        if (nums.size() < 2) return nums.size();
        
        int peaks = 1;
        int prevDiff = 0;
        for (size_t i = 1; i < nums.size(); ++i) {
            int diff = nums[i] - nums[i - 1];
            if ((diff > 0 && prevDiff <= 0) || (diff < 0 && prevDiff >= 0)) {
                peaks++;
                prevDiff = diff;
            }
        }
        return peaks;
    }
};

Maximum Subarray Sum

Strategy: Reset the current sum if it becomes negative, as a negative prefix reduces the total.

class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        int maxSum = INT_MIN;
        int currentSum = 0;
        
        for (int val : nums) {
            currentSum += val;
            if (currentSum > maxSum) {
                maxSum = currentSum;
            }
            if (currentSum <= 0) {
                currentSum = 0;
            }
        }
        return maxSum;
    }
};

Best Time to Buy and Sell Stock II

Strategy: Accumulate all positive profit differences between consecutive days.

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int totalProfit = 0;
        for (size_t i = 1; i < prices.size(); ++i) {
            int diff = prices[i] - prices[i - 1];
            if (diff > 0) {
                totalProfit += diff;
            }
        }
        return totalProfit;
    }
};

Jump Game

Strategy: Track the maximum reachable index. If the current index exceeds the max reach, the end is unreachable.

class Solution {
public:
    bool canJump(vector<int>& nums) {
        int maxReach = 0;
        for (int i = 0; i <= maxReach && i < nums.size(); ++i) {
            maxReach = max(maxReach, i + nums[i]);
            if (maxReach >= nums.size() - 1) return true;
        }
        return false;
    }
};

Jump Game II

Strategy: Track the current range end and the farthest reachable point. Increment steps when the current range is exhausted.

class Solution {
public:
    int jump(vector<int>& nums) {
        if (nums.size() <= 1) return 0;
        
        int steps = 0;
        int currentEnd = 0;
        int farthest = 0;
        
        for (int i = 0; i < nums.size() - 1; ++i) {
            farthest = max(farthest, i + nums[i]);
            if (i == currentEnd) {
                steps++;
                currentEnd = farthest;
                if (currentEnd >= nums.size() - 1) break;
            }
        }
        return steps;
    }
};

Maximize Sum Of Array After K Negations

Strategy: Flip negative numbers to positive starting from the smallest. If operations remain, flip the smallest absolute value.

class Solution {
public:
    int largestSumAfterKNegations(vector<int>& nums, int k) {
        std::sort(nums.begin(), nums.end());
        
        for (int i = 0; i < nums.size() && k > 0; ++i) {
            if (nums[i] < 0) {
                nums[i] = -nums[i];
                k--;
            } else {
                break;
            }
        }
        
        std::sort(nums.begin(), nums.end());
        if (k % 2 == 1) {
            nums[0] = -nums[0];
        }
        
        return accumulate(nums.begin(), nums.end(), 0);
    }
};

Gas Station

Strategy: If the total gas is less than total cost, no solution exists. Otherwise, reset the start point when ever the current tank drops below zero.

class Solution {
public:
    int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
        int totalSurplus = 0;
        int currentSurplus = 0;
        int startPoint = 0;
        
        for (size_t i = 0; i < gas.size(); ++i) {
            int net = gas[i] - cost[i];
            totalSurplus += net;
            currentSurplus += net;
            
            if (currentSurplus < 0) {
                startPoint = i + 1;
                currentSurplus = 0;
            }
        }
        
        return totalSurplus < 0 ? -1 : startPoint;
    }
};

Candy

Strategy: Two passes: left-to-right to satisfy increasing ratings, then right-to-left to satisfy decreasing ratings.

class Solution {
public:
    int candy(vector<int>& ratings) {
        int n = ratings.size();
        vector<int> candies(n, 1);
        
        for (int i = 1; i < n; ++i) {
            if (ratings[i] > ratings[i - 1]) {
                candies[i] = candies[i - 1] + 1;
            }
        }
        
        for (int i = n - 2; i >= 0; --i) {
            if (ratings[i] > ratings[i + 1]) {
                candies[i] = max(candies[i], candies[i + 1] + 1);
            }
        }
        
        return accumulate(candies.begin(), candies.end(), 0);
    }
};

Lemonade Change

Strategy: Track $5 and $10 bills. Prioritize giving $10 + $5 for $20 change to preserve $5 bills.

class Solution {
public:
    bool lemonadeChange(vector<int>& bills) {
        int fiveCount = 0;
        int tenCount = 0;
        
        for (int bill : bills) {
            if (bill == 5) {
                fiveCount++;
            } else if (bill == 10) {
                if (fiveCount > 0) {
                    fiveCount--;
                    tenCount++;
                } else {
                    return false;
                }
            } else {
                if (tenCount > 0 && fiveCount > 0) {
                    tenCount--;
                    fiveCount--;
                } else if (fiveCount >= 3) {
                    fiveCount -= 3;
                } else {
                    return false;
                }
            }
        }
        return true;
    }
};

Reconstruct Queue by Height

Strategy: Sort by height descending, then by k ascending. Insert each person at index k.

class Solution {
public:
    vector<vector<int>> reconstructQueue(vector<vector<int>>& people) {
        sort(people.begin(), people.end(), [](const vector<int>& a, const vector<int>& b) {
            return a[0] == b[0] ? a[1] < b[1] : a[0] > b[0];
        });
        
        list<vector<int>> queue;
        for (const auto& person : people) {
            auto it = queue.begin();
            advance(it, person[1]);
            queue.insert(it, person);
        }
        
        return vector<vector<int>>(queue.begin(), queue.end());
    }
};

Minimum Number of Arrows to Burst Balloons

Strategy: Sort by end coordinate. Merge overlapping intervals to minimize arrows.

class Solution {
public:
    int findMinArrowShots(vector<vector<int>>& points) {
        if (points.empty()) return 0;
        
        sort(points.begin(), points.end(), [](const vector<int>& a, const vector<int>& b) {
            return a[1] < b[1];
        });
        
        int arrows = 1;
        int currentEnd = points[0][1];
        
        for (size_t i = 1; i < points.size(); ++i) {
            if (points[i][0] > currentEnd) {
                arrows++;
                currentEnd = points[i][1];
            }
        }
        return arrows;
    }
};

Non-overlapping Intervals

Strategy: Sort by end coordinate. Count overlaps and remove the interval ending later.

class Solution {
public:
    int eraseOverlapIntervals(vector<vector<int>>& intervals) {
        if (intervals.empty()) return 0;
        
        sort(intervals.begin(), intervals.end(), [](const vector<int>& a, const vector<int>& b) {
            return a[1] < b[1];
        });
        
        int removed = 0;
        int prevEnd = intervals[0][1];
        
        for (size_t i = 1; i < intervals.size(); ++i) {
            if (intervals[i][0] < prevEnd) {
                removed++;
            } else {
                prevEnd = intervals[i][1];
            }
        }
        return removed;
    }
};

Partition Labels

Strategy: Record the last occurrence of each character. Extend the partition until the current index matches the max last occurrence.

class Solution {
public:
    vector<int> partitionLabels(string s) {
        vector<int> lastOccurrence(26, 0);
        for (int i = 0; i < s.size(); ++i) {
            lastOccurrence[s[i] - 'a'] = i;
        }
        
        vector<int> result;
        int start = 0;
        int end = 0;
        
        for (int i = 0; i < s.size(); ++i) {
            end = max(end, lastOccurrence[s[i] - 'a']);
            if (i == end) {
                result.push_back(i - start + 1);
                start = i + 1;
            }
        }
        return result;
    }
};

Merge Intervals

Strategy: Sort by start time. Merge overlapping intervals by updating the end time.

class Solution {
public:
    vector<vector<int>> merge(vector<vector<int>>& intervals) {
        if (intervals.empty()) return {};
        
        sort(intervals.begin(), intervals.end());
        vector<vector<int>> merged;
        merged.push_back(intervals[0]);
        
        for (size_t i = 1; i < intervals.size(); ++i) {
            vector<int>& last = merged.back();
            if (intervals[i][0] <= last[1]) {
                last[1] = max(last[1], intervals[i][1]);
            } else {
                merged.push_back(intervals[i]);
            }
        }
        return merged;
    }
};

Monotone Increasing Digits

Strategy: Find the first decrease, decrement the previous digit, and set all subsequent digits to 9.

class Solution {
public:
    int monotoneIncreasingDigits(int n) {
        string s = to_string(n);
        int marker = s.size();
        
        for (int i = s.size() - 1; i > 0; --i) {
            if (s[i - 1] > s[i]) {
                marker = i;
                s[i - 1]--;
            }
        }
        
        for (int i = marker; i < s.size(); ++i) {
            s[i] = '9';
        }
        
        return stoi(s);
    }
};

Binary Tree Cameras

Strategy: Post-order traversal. States: 0 (Needs Camera), 1 (Has Camera), 2 (Covered). Install camera if a child needs it.

class Solution {
public:
    int cameras = 0;
    
    int traverse(TreeNode* node) {
        if (!node) return 2;
        
        int left = traverse(node->left);
        int right = traverse(node->right);
        
        if (left == 0 || right == 0) {
            cameras++;
            return 1;
        }
        
        if (left == 1 || right == 1) {
            return 2;
        }
        
        return 0;
    }
    
    int minCameraCover(TreeNode* root) {
        if (traverse(root) == 0) cameras++;
        return cameras;
    }
};

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.