Greedy Intro
LeetCode problems solved with Greedy solutions.
Greedy problems may be the hardest because as they all follow a pattern, nor are they always intuitive. You just have to come up with a solution that you think should work and hope that it works for all test cases.
and the counter point:
in greedy, you always start off by using key observations and reasoning to narrow down your "search space" by eliminating possibilities to get to the answer, i.e. finding scenarios where getting the answer is impossible. in this example, it would be realizing a triplet whose value(s) exceed the target are unusable in the merging process and hence can be skipped. from there, you can derive what's possible / what are the "search spaces" that you actually care about and code up the solution accordingly. greedy problems can be quite unstructured, but the code implementation isn't challenging to grasp (like backtracking) or the solution particularly hard to come by (like dp and figuring out the recurrence relation) so it can be a good and fun topic to practice.
What is Greedy
Money!
Greedy IRL
Money!
Greedy Application: Greedy
Pattern: money!
Ex: money!!
def money!(n: int) -> int:
return n+1
53. Maximum Subarray ::3:: - Medium
Topics: Array, Divide and Conquer, Dynamic Programming
Intro
Given an integer array nums, find the with the largest sum, and return its sum.
Follow up: If you have figured out the O(n) solution, try coding another solution using the divide and conquer approach, which is more subtle.
Example Input | Output |
---|---|
nums = [-2,1,-3,4,-1,2,1,-5,4] | 6 |
nums = [1] | 1 |
nums = [5,4,-1,7,8] | 23 |
Constraints:
1 ≤ nums.length ≤ 105
-104 ≤ nums[i] ≤ 104
Abstraction
Given an array, return the sum of the largest subarray.
Space & Time Complexity
Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
---|---|---|---|---|
Bug | Error |
---|---|
Brute Force:
Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
---|---|---|---|---|
Find the Bug:
Solution 1: Greedy Kadane Algorithm Textbook Version - Greedy/Greedy
def maxSubArray(self, nums: List[int]) -> int:
# Note:
# Kadane's Algorithm (Greedy / DP)
# iterate over array
# at every step either extend subarray or start new subarray
# Result -> max sum
n = len(nums)
local_max = nums[0]
global_max = nums[0]
# iterate over array
for i in range(1, n):
# nums[i]: start new subarray at i
# local_max + nums[i]: extend previous subarray
local_max = max(nums[i], local_max + nums[i])
# compare to global max
global_max = max(global_max, local_max)
# overall: time complexity O(n)
# overall: space complexity O(1)
return global_max
Solution 2: Greedy Kadane Algorithm Practical Version - Greedy/Greedy
def maxSubArray(self, nums: List[int]) -> int:
# Note:
# Greedy (Kadane’s Algorithm)
# iterate over array
# at every step either extend subarray or start new subarray
# Result -> max sum
max_sum = nums[0]
current_sum = 0
# iterate over array
for num in nums:
# extend subarray: current_sum + num
current_sum += num
# start new subarray at i
if current_sum < 0:
current_sum = 0
# compare to global max
if current_sum > max_sum:
max_sum = current_sum
# overall: time complexity
# overall: space complexity
return max_sum
Solution 3: Divide and Conquer - Greedy/Greedy
def maxSubArray(self, nums: List[int]) -> int:
# Note:
# Divide & Conquer approach
# 1. Process root -> split array into halves
# 2. Explore Choices -> max subarray is:
# entirely in left half
# entirely in right half
# crosses the midpoint (ending at mid and starting at mid+1)
# 3. Build -> recursively compute left, right, cross sums
# Result -> max sum of array
def helper(left, right):
# single element, return element
if left == right:
return nums[left]
# midpoint for split and cross_max
mid = (left + right) // 2
# check left and right subarray
left_max = helper(left, mid)
right_max = helper(mid + 1, right)
# maximum suffix sum of the left half: best sum ending at mid
left_sum = float('-inf')
curr = 0
for i in range(mid, left - 1, -1):
curr += nums[i]
left_sum = max(left_sum, curr)
# maximum prefix sum of the right half: best sum starting at mid+1
right_sum = float('-inf')
curr = 0
for i in range(mid + 1, right + 1):
curr += nums[i]
right_sum = max(right_sum, curr)
# maximum subarray sum that spans across the midpoint.
cross_max = left_sum + right_sum
# pass max from left, right, and cross section to parent
return max(left_max, right_max, cross_max)
res = helper(0, len(nums) - 1)
# overall: time complexity O(n log n)
# overall: space complexity O(log n) recursion depth
return res
55. Jump Game ::1:: - Medium
Topics: Array, Dynamic Programming, Greedy
Intro
You are given an integer array nums. You are initially positioned at the array's first index, and each element in the array represents your maximum jump length at that position. Return true if you can reach the last index, or false otherwise.
Example Input | Output |
---|---|
nums = [2,3,1,1,4] | true |
nums = [3,2,1,0,4] | false |
Constraints:
1 ≤ nums.length ≤ 104
0 ≤ nums[i] ≤ 105
Abstraction
Given an array, starting at the first index, determine if its possible to reach the end of the array, if a number at an index represents that max jump index possible from that index.
Space & Time Complexity
Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
---|---|---|---|---|
Bug | Error |
---|---|
Brute Force:
Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
---|---|---|---|---|
Find the Bug:
Solution 1: Greedy Dynamic Programming Rolling Variable Early Exit - Greedy/Greedy
def canJump(self, nums: List[int]) -> bool:
# Note:
#
# Rolling Variable -> tracks farthest possible index we can reach
# Iterate over array ->
# 2. Build From Previous -> maintain farthest reachable index so far
# 3. Explore Choices -> if current index > farthest, return False (stuck)
# 4. Result -> if iteration completes, last index is reachable
n = len(nums)
# rolling variable
farthest = 0
# iterate array
for i in range(n):
# reached an unreachable index, cannot reach end
if i > farthest:
return False
# Build From Previous -> update farthest reachable
farthest = max(farthest, i + nums[i])
# Early exit -> can already reach or surpass last index
if farthest >= n - 1:
return True
# last index is not reachable
# overall: time complexity
# overall: space complexity
return False
45. Jump Game II ::1:: - Medium
Topics: Array, Dynamic Programming, Greedy
Intro
You are given a 0-indexed array of integers nums of length n.
You are initially positioned at index 0. Each element nums[i] represents the maximum length of a forward jump from index i. In other words, if you are at index i, you can jump to any index (i + j) where: 0 < = j < = nums[i] and i + j < n Return the minimum number of jumps to reach index n - 1. The test cases are generated such that you can reach index n - 1.
Example Input | Output |
---|---|
nums = [2,3,1,1,4] | 2 |
nums = [2,3,0,1,4] | 2 |
Constraints:
1 ≤ nums.length ≤ 104
0 ≤ nums[i] ≤ 1000
It's guaranteed that you can reach nums[n - 1].
Abstraction
Given an array, starting at the first index, determine if its possible to reach the end of the array, if a number at an index represents that max jump index possible from that index.
Space & Time Complexity
Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
---|---|---|---|---|
Bug | Error |
---|---|
Brute Force:
Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
---|---|---|---|---|
Find the Bug:
Solution 1: Greedy Layer By Layer - Greedy/Greedy
def jump(self, nums: List[int]) -> int:
# Note:
# Greedy (layer expansion like BFS)
# A layer in this context is a BFS level of the array.
# Each level contains all indices you can reach with the same number
# of jumps.
n = len(nums)
# no jumps needed
if n == 1:
return 0
# Each level contains all indices you can reach with the same number
# of jumps.
jumps = 0
# current_end marks the rightmost index in the current layer.
# So when we reach current_end, it means we have finished exploring the
# current layer, and current_end holds the best next move for this layer.
current_end = 0
# keeps track of the furthest index we can reach in this layer.
farthest = 0
# iterate -> 0 to n-2
# problem guarantees we can always reach the last index,
# so we don't need to check for unreachable branches
for i in range(n-1):
# update farthest reachable index for index
farthest = max(farthest, i + nums[i])
# reach end of current layer, go to next layer
if i == current_end:
jumps += 1 # need to jump
current_end = farthest # move to next layer
# Result -> total jumps
# overall: time complexity
# overall: space complexity
return jumps
134. Gas Station ::1:: - Medium
Topics: Array, Greedy
Intro
There are n gas stations along a circular route, where the amount of gas at the ith station is gas[i]. You have a car with an unlimited gas tank and it costs cost[i] of gas to travel from the ith station to its next (i + 1)th station. You begin the journey with an empty tank at one of the gas stations. Given two integer arrays gas and cost, return the starting gas station's index if you can travel around the circuit once in the clockwise direction, otherwise return -1. If there exists a solution, it is guaranteed to be unique.
Example Input | Output |
---|---|
gas = [1,2,3,4,5], cost = [3,4,5,1,2] | 3 |
gas = [2,3,4], cost = [3,4,3] | -1 |
Constraints:
n == gas.length == cost.length
1 ≤ nums[i] ≤ 105
0 ≤ gas[i], cost[i] ≤ 104
The input is generated such that the answer is unique.
Abstraction
Given two arrays gas[i] and cost[i], find the starting index where if you begin with 0 fuel, you can travel the entire circle without running out of gas. Return -1 if not possible.
Space & Time Complexity
Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
---|---|---|---|---|
Bug | Error |
---|---|
Brute Force:
Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
---|---|---|---|---|
Find the Bug:
Solution 1: Greedy - Greedy/Greedy
def canCompleteCircuit(self, gas: List[int], cost: List[int]) -> int:
# Note:
# Greedy Linear Scan
# 0. Direct Boundary -> if total gas < total cost, impossible (-1)
# 1. Process root -> traverse stations
# 2. Build From Previous -> track current tank balance
# 3. Explore Choices -> if tank < 0, reset start to next station
# 4. Result -> return start index (guaranteed unique if feasible)
n = len(gas)
# not enough total gas to cover total cost
if sum(gas) < sum(cost):
return -1
# current gas
tank = 0
# candidate starting station
start = 0
# try each starting station
for i in range(n):
# Question guarantees solution to be unique, so:
# you don't need to try every starting point individually.
# As you iterate, you maintain a running tank balance.
# You never actually traverse every “branch”, but the greedy
# reset logic ensures the feasible branch is picked.
# update tank balance
tank += gas[i] - cost[i]
# reset start if tank is negative
# try next start candidate
if tank < 0:
tank = 0
start = i + 1
# Result -> guaranteed unique if feasible
# overall: time complexity
# overall: space complexity
return start
846. Hand of Straights ::1:: - Medium
Topics: Array, Hash Table, Greedy, Sorting
Intro
Alice has some number of cards and she wants to rearrange the cards into groups so that each group is of size groupSize, and consists of groupSize consecutive cards. Given an integer array hand where hand[i] is the value written on the ith card and an integer groupSize, return true if she can rearrange the cards, or false otherwise.
Example Input | Output |
---|---|
hand = [1,2,3,6,2,3,4,7,8], groupSize = 3 | true |
hand = [1,2,3,4,5], groupSize = 4 | false |
Constraints:
1 ≤ hand.length ≤ 104
0 ≤ hand[i] ≤ 109
1 ≤ groupSize ≤ hand.length
Abstraction
Given an array and a run length, determine if its possible to rearrange the cards of run length that are increasing.
Space & Time Complexity
Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
---|---|---|---|---|
Bug | Error |
---|---|
Brute Force:
Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
---|---|---|---|---|
Find the Bug:
Solution 1: Greedy - Greedy/Greedy
def isNStraightHand(self, hand: List[int], groupSize: int) -> bool:
# Note:
# Greedy with HashMap
# total cards must be divisible by groupSize
# sort cards
# group cards from low to high
# for each group start, grab all cards needed in group
# Result -> return True if all groups formed
n = len(hand)
# total cards must be divisible by groupSize
if n % groupSize != 0:
return False
# frequency of each card
count = Counter(hand)
# process cards in ascending order
for runStartCard in sorted(count):
# attempt to form group starting at card
while count[runStartCard] > 0:
# attempt to grab all cards for current run starting from card
for nextCard in range(runStartCard, runStartCard + groupSize):
# no run is possible
if count[nextCard] == 0:
return False
# decrease count
count[nextCard] -= 1
# overall: time complexity
# overall: space complexity
return True
Solution 2: Greedy Heap - Greedy/Greedy
def isNStraightHand(self, hand: List[int], groupSize: int) -> bool:
# Note:
# Greedy using min heap and frequency map
# 0. Direct Boundary -> total cards must be divisible by groupSize
# 1. Process root -> build frequency map and min-heap of unique cards
# 2. Build From Previous -> repeatedly form groups starting from smallest card
# 3. Explore Choices -> check consecutive cards and update frequencies
# Result -> return True if all groups can be formed
n = len(hand)
# check if total cards can be divided into complete groups
if n % groupSize != 0:
return False
# frequency map of cards
counter = {}
for h in hand:
counter[h] = 1 + counter.get(h, 0)
# min heap of unique cards
heap = list(counter.keys())
heapq.heapify(heap)
# repeatedly form groups
while heap:
first = heap[0] # smallest available card
# try to form a consecutive run starting from first
for h in range(first, first + groupSize):
# cannot form run if card missing
if h not in counter:
return False
# decrement frequency
counter[h] -= 1
# remove from heap if no cards left
if counter[h] == 0:
# ensure heap order is maintained
if h != heap[0]:
return False
heapq.heappop(heap)
# all runs successfully formed
# overall: time complexity
# overall: space complexity
return True
1899. Merge Triplets to Form Target Triplet ::1:: - Medium
Topics: Array, Greedy
Intro
A triplet is an array of three integers. You are given a 2D integer array triplets, where triplets[i] = [ai, bi, ci] describes the ith triplet. You are also given an integer array target = [x, y, z] that describes the
triplet you want to obtain. To obtain target, you may apply the following operation on triplets any number of times (possibly zero): Choose two indices (0-indexed) i and j (i != j) and update triplets[j] to become [max(ai, aj), max(bi, bj), max(ci, cj)]. For example, if triplets[i] = [2, 5, 3] and triplets[j] = [1, 7, 5], triplets[j] will be updated to [max(2, 1), max(5, 7), max(3, 5)] = [2, 7, 5]. Return true if it is possible to obtain the target triplet [x, y, z] as an element of triplets, or false otherwise.
Example Input | Output |
---|---|
triplets = [[2,5,3],[1,8,4],[1,7,5]], target = [2,7,5] | true |
triplets = [[3,4,5],[4,5,6]], target = [3,2,5] | false |
triplets = [[2,5,3],[2,3,4],[1,2,5],[5,2,3]], target = [5,5,5] | true |
Constraints:
1 ≤ triplets.length ≤ 105
triplets[i].length == target.length == 3
1 ≤ ai, bi, ci, x, y, z ≤ 1000
Abstraction
Given a list of triplets and a target triplet, determine if you can select and merge triplets such that each element of the target is matched exactly by taking the maximum across chosen triplets.
Space & Time Complexity
Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
---|---|---|---|---|
Bug | Error |
---|---|
Brute Force:
Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
---|---|---|---|---|
Find the Bug:
Solution 1: Greedy - Greedy/Greedy
def mergeTriplets(self, triplets: List[List[int]], target: List[int]) -> bool:
# Note:
# Greedy validation
# instead of simulating all operations, you just need to
# ensure that for each coordinate of the target, there is at least
# one triplet that contributes the required value without exceeding it.
# grab target values
x, y, z = target
# coverage of each target index
good = [False, False, False]
for a, b, c in triplets:
# skip if triplet exceeds target in any dimension as
# merging triplet would overshoot the target since we use max()
if a > x or b > y or c > z:
continue
# mark contribution toward target dimensions
if a == x:
good[0] = True
if b == y:
good[1] = True
if c == z:
good[2] = True
# early exit -> all dimensions matched
if all(good):
return True
# overall: time complexity O(n)
# overall: space complexity O(1)
return all(good)
763. Partition Labels ::1:: - Medium
Topics: Hash Table, Two Pointers, String, Greedy
Intro
You are given a string s. We want to partition the string string into as many parts as possible so that each letter appears in at most one part. For example, the string "ababcc" can be partitioned into ["abab", "cc"], but partitions such as ["aba", "bcc"] or ["ab", "ab", "cc"] are invalid. Note that the partition is done so that after concatenating all the parts in order, the resultant string should be s. Return a list of integers representing the size of these parts.
Example Input | Output |
---|---|
s = "ababcbacadefegdehijhklij" | [9, 7, 8] |
s = "eccbbbbdec" | [10] |
Constraints:
1 ≤ s.length ≤ 500
s consists of lowercase English letters.
Abstraction
Given a string, determine what length the subarray should be in order to have every character appear in each subarray
Space & Time Complexity
Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
---|---|---|---|---|
Bug | Error |
---|---|
Brute Force:
Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
---|---|---|---|---|
Find the Bug:
Solution 1: Greedy - Greedy/Greedy
def partitionLabels(self, s: str) -> List[int]:
# Note:
# Greedy partitioning
# 0. Direct Length -> empty string boundary covered implicitly
# 1. Precompute last occurrence of each character
# 2. Process root -> scan through string with two pointers
# 3. Expand window -> current partition must at least reach the furthest last occurrence seen so far
# 4. Build Partition -> when current index == window end, cut partition
# 5. Result -> return lengths of all partitions
# effectively gives the last occurrence of each character in the string,
# as last occurrence of char will overwrite index previous
last_index = {char: i for i, char in enumerate(s)}
partitions = []
# starting index of current partition
start = 0
# Furthest index that the current partition must reach to
# include all characters seen so far
# Thus, when the iteration index i reaches end, it means the
# current partition contains all occurrences of the characters seen
# in this window, so you can "cut" the partition:
end = 0
# iterate left to right
for i, char in enumerate(s):
# As we go left to right, if a character appears multiple times,
# the dictionary keeps updating its value to the latest index
# ensures the current partition extends at least until the
# last occurrence of every character seen so far.
end = max(end, last_index[char])
# reached the last occurrence of every character seen so far,
# safe to cut
if i == end:
partitions.append(end - start + 1)
start = i + 1
# overall: time complexity
# overall: space complexity
return partitions
678. Valid Parenthesis String ::2:: - Medium
Topics: String, Dynamic Programming, Stack, Greedy
Intro
Given a string s containing only three types of characters: '(', ')' and '', return true if s is valid. The following rules define a valid string: Any left parenthesis '(' must have a corresponding right parenthesis ')'. Any right parenthesis ')' must have a corresponding left parenthesis '('. Left parenthesis '(' must go before the corresponding right parenthesis ')'. '' could be treated as a single right parenthesis ')' or a single left parenthesis '(' or an empty string "".
Example Input | Output |
---|---|
s = "()" | true |
s = "(*)" | true |
s = "(*))" | true |
Constraints:
1 ≤ s.length ≤ 100
s[i] is '(', ')' or '*'
Abstraction
Given a string, determine if the parenthesis are valid.
Space & Time Complexity
Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
---|---|---|---|---|
Bug | Error |
---|---|
Brute Force:
Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
---|---|---|---|---|
Find the Bug:
Solution 1: Greedy - Greedy/Greedy
def checkValidString(self, s: str) -> bool:
# Note:
# Greedy balance with range tracking
# empty string is valid (covered implicitly)
# maintain lower and upper bound of open '(' count
# Bounds:
# '(' increases both lower and upper
# ')' decreases both (but lower cannot go below 0)
# '*' can be '(', ')' or empty
# -> lower decreases by 1 (min 0), upper increases by 1
# if upper < 0 at any point, invalid
# Result -> valid if lower == 0 at the end
# parenthesis count
lower = 0
upper = 0
# iterate
for char in s:
# '(' increases both lower and upper
if char == "(":
lower += 1
upper += 1
# ')' decreases both (but lower cannot go below 0)
elif char == ")":
lower = max(lower - 1, 0)
upper -= 1
# '*' can be '(', ')' or empty
# lower decreases by 1 (min 0), upper increases by 1
else:
lower = max(lower - 1, 0) # treat as ')'
upper += 1 # treat as '('
# if upper < 0 at any point, invalid
if upper < 0:
return False
# lower == 0 means all opens can be matched
# overall: time complexity
# overall: space complexity
return lower == 0