Greedy Algorithms in TheAlgorithms/Python
Five problems where the locally optimal choice at each step produces the globally optimal result.
What you will learn
- How fractional_knapsack sorts by value-per-weight ratio and uses bisect to find the item that fills the remaining capacity
- How minimum_coin_change iterates denominations from largest to smallest and greedily subtracts each while it fits
- How max_profit tracks the running minimum price and maximum profit in a single pass without nested loops
- How can_complete_journey checks global feasibility first and then scans for the starting index using running net-gas
- How optimal_merge_pattern repeatedly merges the two smallest file sizes, accumulating cost in a greedy minimum-first order
Prerequisites
- Understanding of what a sorted list and a greedy choice property mean
- Familiarity with basic Python list operations and comprehensions
Fractional knapsack: sort by value-per-weight, bisect to the filling item
greedy_methods/fractional_knapsack.py:5Sorting by value-per-weight descending is the greedy strategy; bisect on the cumulative weight list finds the last fully packed item in O(log n).
The fractional knapsack differs from the 0/1 knapsack (covered in the DP category) by allowing items to be split. That one change makes a greedy approach correct: always take as much as possible of the highest value-per-weight item. The function sorts the zipped value-weight pairs by value / weight in descending order, then rebuilds vl and wt in that order. accumulate(wt) builds the prefix sum of weights so acc[i] is the total weight if you take items 0 through i. bisect(acc, w) finds how many items fit completely within the weight limit w. If all items fit (k == n), the return is their full value. If some remain, the last item is taken fractionally: (w - acc[k - 1]) is the remaining capacity, multiplied by the value density vl[k] / wt[k] of the next item.
Fractional knapsack is a sort plus a bisect: sort by value density descending, locate the fill boundary with binary search, then take the last item fractionally.
def frac_knapsack(vl, wt, w, n):
"""
>>> frac_knapsack([60, 100, 120], [10, 20, 30], 50, 3)
240.0
>>> frac_knapsack([10, 40, 30, 50], [5, 4, 6, 3], 10, 4)
105.0
>>> frac_knapsack([10, 40, 30, 50], [5, 4, 6, 3], 8, 4)
95.0
>>> frac_knapsack([10, 40, 30, 50], [5, 4, 6], 8, 4)
60.0
>>> frac_knapsack([10, 40, 30], [5, 4, 6, 3], 8, 4)
60.0
>>> frac_knapsack([10, 40, 30, 50], [5, 4, 6, 3], 0, 4)
0
>>> frac_knapsack([10, 40, 30, 50], [5, 4, 6, 3], 8, 0)
95.0
>>> frac_knapsack([10, 40, 30, 50], [5, 4, 6, 3], -8, 4)
0
>>> frac_knapsack([10, 40, 30, 50], [5, 4, 6, 3], 8, -4)
95.0
>>> frac_knapsack([10, 40, 30, 50], [5, 4, 6, 3], 800, 4)
130
>>> frac_knapsack([10, 40, 30, 50], [5, 4, 6, 3], 8, 400)
95.0
>>> frac_knapsack("ABCD", [5, 4, 6, 3], 8, 400)
Traceback (most recent call last):
...Minimum coin change: scan denominations largest-to-smallest, subtract while fits
greedy_methods/minimum_coin_change.py:44The greedy strategy for coin change on canonical denomination sets is to always use the largest denomination that fits in the remaining amount.
The greedy coin change algorithm works correctly when the denomination set is canonical, meaning each denomination is at least double the previous one or follows a specific structure that prevents suboptimal choices. Indian currency denominations (1, 2, 5, 10, 20, 50, 100, 200, 500, 2000) have this property, which is why the doctests use them. The algorithm reverses the sorted denomination list so the largest value comes first, then repeatedly subtracts each denomination from the remaining total for as long as the denomination fits. The inner while loop handles cases like 18745, where 2000 is used nine times before 500 becomes the next fit. This greedy approach fails for arbitrary denomination sets; a denomination set like (1, 3, 4) with target 6 would require a DP solution to find the optimal (3, 3) rather than the greedy (4, 1, 1).
Greedy coin change is correct only for canonical denomination sets; iterate from largest to smallest and subtract until the denomination no longer fits.
def find_minimum_change(denominations: list[int], value: str) -> list[int]:
"""
Find the minimum change from the given denominations and value
>>> find_minimum_change([1, 5, 10, 20, 50, 100, 200, 500, 1000,2000], 18745)
[2000, 2000, 2000, 2000, 2000, 2000, 2000, 2000, 2000, 500, 200, 20, 20, 5]
>>> find_minimum_change([1, 2, 5, 10, 20, 50, 100, 500, 2000], 987)
[500, 100, 100, 100, 100, 50, 20, 10, 5, 2]
>>> find_minimum_change([1, 2, 5, 10, 20, 50, 100, 500, 2000], 0)
[]
>>> find_minimum_change([1, 2, 5, 10, 20, 50, 100, 500, 2000], -98)
[]
>>> find_minimum_change([1, 5, 100, 500, 1000], 456)
[100, 100, 100, 100, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 1]
"""
total_value = int(value)
# Initialize Result
answer = []
# Traverse through all denomination
for denomination in reversed(denominations):
# Find denominations
while int(total_value) >= int(denomination):
total_value -= int(denomination)
answer.append(denomination) # Append the "answers" array
return answerBest time to buy and sell stock: track running minimum and maximum profit in one pass
greedy_methods/best_time_to_buy_and_sell_stock.py:18The greedy insight is that the best sell day for any buy day is when the profit is maximum; tracking the running minimum buy price captures this without nested loops.
The stock profit problem asks for the maximum single buy-sell pair profit given a price series. A naive solution tries every buy-sell pair in O(n squared). The greedy observation is that to maximize profit on a sell day, you want to have bought at the lowest price seen so far. The algorithm maintains two running values: min_price tracks the cheapest buy opportunity seen up to the current day, and max_profit tracks the best profit achievable so far. On each iteration, min_price is updated first so it reflects the cheapest price on or before the current day, then max_profit compares the profit from selling today against the current best. The second doctest confirms the correct handling of a monotonically decreasing price series: no profitable trade exists, so the answer is 0 rather than a negative value.
Tracking the running minimum buy price and comparing against each subsequent price gives the optimal answer in one pass, making nested pair comparison unnecessary.
def max_profit(prices: list[int]) -> int:
"""
>>> max_profit([7, 1, 5, 3, 6, 4])
5
>>> max_profit([7, 6, 4, 3, 1])
0
"""
if not prices:
return 0
min_price = prices[0]
max_profit: int = 0
for price in prices:
min_price = min(price, min_price)
max_profit = max(price - min_price, max_profit)
return max_profitGas station: check global feasibility, then find the start by tracking net gas
greedy_methods/gas_station.py:62If total gas is sufficient, a valid start always exists; the greedy scan resets the candidate start whenever net gas goes negative.
The gas station problem asks for a starting station from which a car can complete a circular route without running out of fuel. A brute-force solution tries every starting station in O(n squared). The greedy algorithm exploits a key insight: if total gas across all stations is at least total cost, a valid starting station is guaranteed to exist. Lines 80-81 compute both sums. If total gas falls short, the function returns -1 immediately. Otherwise it scans stations, tracking the running net gas from a candidate start. When net gas drops below 0, the candidate cannot work, so it resets to the next station and net gas resets to 0. Because global feasibility is confirmed, the final candidate is guaranteed valid. The GasStation dataclass pairs each station's gas quantity and travel cost.
Check global feasibility first: if total gas covers total cost, the greedy scan guarantees a valid start by resetting the candidate whenever net gas goes negative.
def can_complete_journey(gas_stations: tuple[GasStation, ...]) -> int:
"""
This function returns the index from which to start the journey
in order to reach the end.
Args:
gas_quantities [list]: Amount of gas available at each station
cost [list]: The cost of gas required to move from one station to the next
Returns:
start [int]: start index needed to complete the journey
Examples:
>>> can_complete_journey(get_gas_stations([1, 2, 3, 4, 5], [3, 4, 5, 1, 2]))
3
>>> can_complete_journey(get_gas_stations([2, 3, 4], [3, 4, 3]))
-1
"""
total_gas = sum(gas_station.gas_quantity for gas_station in gas_stations)
total_cost = sum(gas_station.cost for gas_station in gas_stations)Optimal merge pattern: always merge the two smallest files first
greedy_methods/optimal_merge_pattern.py:23Always merging the two smallest files minimizes total merge cost because smaller files are included in more subsequent merges; the greedy choice is provably optimal.
Merging sorted files costs the sum of their sizes per pairwise merge. The order determines total cost because small files merged early are included in every subsequent merge. The greedy strategy is to always merge the two smallest files, minimizing small-file contributions. Each iteration calls files.index(min(files)) twice, removes both minimums, and appends their combined size. That combined size also accumulates into optimal_merge_cost. For files [2, 3, 4]: first merge combines 2 and 3 into 5 at cost 5; second merges 4 and 5 at cost 9; total is 14. A different order (merging 3 and 4 first) gives total 16. A production version would use a min-heap to replace the repeated O(n) scans with O(log n) extractions.
Merging smallest files first is greedy-optimal because small files contribute to every subsequent merge; a min-heap would reduce the O(n squared) scan to O(n log n).
def optimal_merge_pattern(files: list) -> float:
"""Function to merge all the files with optimum cost
Args:
files [list]: A list of sizes of different files to be merged
Returns:
optimal_merge_cost [int]: Optimal cost to merge all those files
Examples:
>>> optimal_merge_pattern([2, 3, 4])
14
>>> optimal_merge_pattern([5, 10, 20, 30, 30])
205
>>> optimal_merge_pattern([8, 8, 8, 8, 8])
96
"""
optimal_merge_cost = 0
while len(files) > 1:
temp = 0
# Consider two files with minimum cost to be merged
for _ in range(2):
min_index = files.index(min(files))
temp += files[min_index]
files.pop(min_index)
files.append(temp)
optimal_merge_cost += temp
return optimal_merge_costYou've walked through 5 key areas of the The Algorithms - Python codebase.
Continue: Divide and Conquer Algorithms in TheAlgorithms/Python → Browse all projectsCreate code tours for your project
Intraview lets AI create interactive walkthroughs of any codebase. Install the free VS Code extension and generate your first tour in minutes.
Install Intraview Free