Recursive Weight Plate Calculator Explained
This is a step-by-step walkthrough of a recursive function that calculates which weight plates are needed to reach a target weight. It’s a practical example that shows how recursion builds results from the bottom up.
The Problem
Given a set of weight plates [25, 20, 15, 10, 5, 2.5] kg, determine which plates are needed to reach a target weight. If the target is impossible with these plates, return None.
The Solution
plates = [25, 20, 15, 10, 5, 2.5]
def weight_calc_clean(base, index=0):
# BASE CASE 1: We hit the target exactly
if base == 0:
return {}
# BASE CASE 2: We ran out of plates but still have weight left (Impossible)
if index >= len(plates):
return None
# Calculate how many of the CURRENT plate fit
count = int(base // plates[index])
rem_weight = base % plates[index] # Using modulo for the remainder
# Move to the NEXT plate index
result = weight_calc_clean(rem_weight, index + 1)
# If the path below us was successful, add our plates to the tally
if result is not None:
if count > 0:
result[plates[index]] = count
return result
return None
print(f"35kg: {weight_calc_clean(35)}")
print(f"3kg (Impossible): {weight_calc_clean(3)}")
Output:
35kg: {25.0: 1, 10.0: 1}
3kg (Impossible): None
Understanding the Parameters
The function receives two arguments:
| Parameter | Meaning |
|---|---|
base |
The weight still to be accounted for |
index |
Which plate we’re currently considering (0 = 25kg, 1 = 20kg, etc.) |
The plates list serves as our lookup table:
- Index 0 → 25 kg plate
- Index 1 → 20 kg plate
- Index 2 → 15 kg plate
- And so on…
The Three Logical Blocks
Base Case 1 — Nothing left to fill (if base == 0)
When the remaining weight hits zero, we’ve found a valid combination. Return an empty dictionary {}. This is the success signal that starts the unwinding process.
Base Case 2 — We ran out of plates (if index >= len(plates))
We’ve worked through every plate size and still have weight left over. The target is impossible with these plates. Return None.
Recursive Case — The Working Step
count = int(base // plates[index]) # How many of this plate fit
rem_weight = base % plates[index] # What's left over
result = weight_calc_clean(rem_weight, index + 1) # Try smaller plates
- Floor division (
//) calculates how many of the current plate fit - Modulo (
%) finds what remains after using those plates - The function calls itself with the remainder and the next plate index
The frame pauses at that line and waits until the recursive call returns. When it does:
- If the result is not
Noneand we used at least one of this plate (count > 0) - Add this plate to the dictionary
- Return the result upward
Step-by-Step Execution: weight_calc_clean(35)
Call 1 — Considering the 25 kg plate
base = 35, index = 0, plates[0] = 25
35 // 25 = 1 → count = 1 (one 25kg plate fits)
35 % 25 = 10 → rem_weight = 10 (ten kilograms still to account for)
Call: weight_calc_clean(10, 1)
Call 2 — Considering the 20 kg plate
base = 10, index = 1, plates[1] = 20
10 // 20 = 0 → count = 0 (20kg plate doesn't fit)
10 % 20 = 10 → rem_weight = 10 (nothing subtracted)
Call: weight_calc_clean(10, 2)
Call 3 — Considering the 15 kg plate
base = 10, index = 2, plates[2] = 15
10 // 15 = 0 → count = 0 (15kg plate doesn't fit)
10 % 15 = 10 → rem_weight = 10 (still the same)
Call: weight_calc_clean(10, 3)
Call 4 — Considering the 10 kg plate
base = 10, index = 3, plates[3] = 10
10 // 10 = 1 → count = 1 (one 10kg plate fits exactly)
10 % 10 = 0 → rem_weight = 0 (nothing left!)
Call: weight_calc_clean(0, 4)
Call 5 — Base Case 1 Triggers!
base = 0
Condition: if base == 0 is TRUE
Return: {} (empty dictionary)
The recursion stops going deeper. This is the moment the stack begins to unwind.
Understanding Unwinding
Up to this point, every call has been paused, waiting for the call below it to finish. Think of them as a stack of sticky notes, each saying “I’ll finish my job once I hear back from the one below me.”
When Call 5 returns {}, that bottom sticky note is removed, and Call 4 wakes up.
Unwinding Call 4 — The 10 kg plate
Receives: result = {} from Call 5
result is not None → enter the block
count = 1, which is > 0
Execute: result[10] = 1
Dictionary is now: {10: 1}
Return upward to Call 3
Unwinding Call 3 — The 15 kg plate
Receives: result = {10: 1}
result is not None → enter the block
count = 0, which is NOT > 0
Nothing added to dictionary
Dictionary remains: {10: 1}
Return upward to Call 2
Unwinding Call 2 — The 20 kg plate
Receives: result = {10: 1}
result is not None → enter the block
count = 0, which is NOT > 0
Nothing added to dictionary
Dictionary remains: {10: 1}
Return upward to Call 1
Unwinding Call 1 — The 25 kg plate
Receives: result = {10: 1}
result is not None → enter the block
count = 1, which is > 0
Execute: result[25] = 1
Dictionary is now: {10: 1, 25: 1}
Stack is fully unwound!
Final Output: {25.0: 1, 10.0: 1}
The Impossible Case: weight_calc_clean(3)
3 kg cannot be assembled from these plates (the smallest is 2.5 kg, and 3 is not a multiple of it).
Plates 25, 20, 15, 10, 5 → all produce count = 0, rem_weight = 3
Index 5 (2.5 kg plate): 3 % 2.5 = 0.5, count = 1, rem_weight = 0.5
Call: weight_calc_clean(0.5, 6)
Now index >= len(plates) is TRUE — we’ve exhausted the plate list with weight still remaining. Base Case 2 triggers and returns None.
Every paused frame wakes up, checks if result is not None, finds it false, skips the block, and falls through to return None. The None propagates unchanged all the way up the stack.
Key Insight
The result dictionary is built from the bottom up:
- The deepest successful call returns an empty
{} - Each frame above adds its own plate count on the way back up
- Frames with
count = 0simply pass the dictionary through untouched
This means the output is clean by construction — no post-processing filter needed, and no zero-count entries cluttering the result.
Summary
| Concept | What It Means |
|---|---|
| Base Case 1 | base == 0 → Success! Return {} |
| Base Case 2 | index >= len(plates) → Impossible! Return None |
| Recursive Step | Calculate plates needed, recurse with remainder |
| Unwinding | Build the dictionary on the way back up |
| Clean Output | Only plates with count > 0 appear in the result |