Background
I’ll go over some pains I’ve had, then create a question list to explore to get better at greedy.
My ELO has essentially been hardstuck for a while because when I see an intermediately difficult greedy problem I sort of give up. The solution always feels very random to me, and it could also be DP. An example is uncrossed lines, which I thought was a greedy at first because the state looked too big, then it was a DP. Sometimes it’s too easy, like maximize points after choosing K tasks I did almost instantly because I happened to see the solution. Or it feels like an obscure greedy pattern like maximum number of weeks then you look at the comments and its people complaining that you should ‘just know’ the answer.
I’m not going to be writing about how to solve all greedy problems, just thoughts about it, then an attempt to break them into categories.
Establishing DP vs Greedy
The worst part of this is people will just say it’s intuition that you build, and the obscure greedy problems never really end. The easiest way to rule out DP is to check your time complexities. DP will put forwards some small maximum space bound, and that can be a hint, then you calculate that effectively brute forcing the problem is still fast enough. Most problems can be identified like that, like coin change, jump game, or gas station.
However, sometimes your state seems cursed. In the last examples, your state could be summarised very shortly. In uncrossed lines, the pain I was having was I’d try to grow from previous states, where each state is a line. So for each line, I’d compare to every previous line, check whether it’s allowed to grow off it, and then try.
class Solution:
def maxUncrossedLines(self, nums1: List[int], nums2: List[int]) -> int:
n, m = len(nums1), len(nums2)
grid = [[0 for _ in range(m)] for _ in range(n)]
for i, j in product(range(n), range(m)):
extra = 1 if nums1[i] == nums2[j] else 0
grid[i][j] = max(grid[i][j], extra)
for x, y in product(range(i), range(j)):
if i > x and j < y:
continue
grid[i][j] = max(grid[x][y] + extra, grid[i][j])
best = 0
for row in grid: best = max(best, max(row))
return bestThe fundamentally wrong thing here is that I didn’t start with the recursion. I tried to do the solution directly on the grid representation, and I didn’t identify that I don’t need to look at all previous states.
Think about what you need for your current state, and how your current state moves forwards. In this case you can move i forwards, or j, or both. So your current state is just , and this has some previous state that was optimal that you took. DP itself is very doable, the issue is just when you’re confusing it for greedy.
Greedy Patterns
Let’s go over some classical greedy problems, then break them into patterns. This list is from the neetcode roadmap, leetcode patterns, and some from the greedy leetcode list.
If it’s your first time seeing these, it’s ok. I propose this set should be studied for a basis of greedy problems, rather than
- Interval Management
- Deadline & Scheduling
- Lexicographical String Construction
- Monotonic Stack Construction
- Tree-Based Greedy
- Constructive Arrays
- Allocation & Binary Search on Answer
- Two-Dimension & Sorting
- Graph-Based Greedy
- Cost/Profit Optimization
- Meeting Rooms & Resource Allocation
- Meeting Rooms II (Premium)
- Car Pooling
- Greedy with Subsequences
- Simulation & Arrangement
- Frequency & Spacing
- Mathematical Optimality
- Partitioning & Grouping
- Swap Optimization
- Backward Processing
- Palindrome Construction
- Parentheses & Balancing
Conclusion
Most other websites just describes greedy as “taking the next best move” and you need to think creatively to find that. I’m proposing a set of questions that can be studied to begin with. You can study the first, then solve the second.
zyros