Files
codetutor/backend/data/questions/candy.yaml

225 lines
10 KiB
YAML

title: Candy
slug: candy
difficulty: hard
leetcode_id: 135
leetcode_url: https://leetcode.com/problems/candy/
categories:
- arrays
patterns:
- slug: greedy
is_optimal: true
function_signature: "def candy(ratings: list[int]) -> int:"
test_cases:
visible:
- input: { ratings: [1, 0, 2] }
expected: 5
- input: { ratings: [1, 2, 2] }
expected: 4
hidden:
- input: { ratings: [1] }
expected: 1
- input: { ratings: [1, 2, 3, 4, 5] }
expected: 15
- input: { ratings: [5, 4, 3, 2, 1] }
expected: 15
- input: { ratings: [1, 3, 2, 2, 1] }
expected: 7
- input: { ratings: [1, 2, 3, 1, 0] }
expected: 9
- input: { ratings: [1, 3, 4, 5, 2] }
expected: 11
- input: { ratings: [1, 1, 1, 1] }
expected: 4
description: |
There are `n` children standing in a line. Each child is assigned a rating value given in the integer array `ratings`.
You are giving candies to these children subjected to the following requirements:
- Each child must have at least one candy.
- Children with a higher rating get more candies than their neighbors.
Return *the minimum number of candies you need to have to distribute the candies to the children*.
constraints: |
- `n == ratings.length`
- `1 <= n <= 2 * 10^4`
- `0 <= ratings[i] <= 2 * 10^4`
examples:
- input: "ratings = [1,0,2]"
output: "5"
explanation: "You can allocate to the first, second and third child with 2, 1, 2 candies respectively."
- input: "ratings = [1,2,2]"
output: "4"
explanation: "You can allocate to the first, second and third child with 1, 2, 1 candies respectively. The third child gets 1 candy because it satisfies the above two conditions."
explanation:
intuition: |
Imagine standing in a line of children where you need to hand out candies fairly based on their ratings. The tricky part is that each child's candy count depends on *both* neighbours — the one to their left and the one to their right.
The key insight is to **break the problem into two simpler sub-problems**: first, ensure each child has more candies than their *left* neighbour (if they have a higher rating), then ensure each child has more candies than their *right* neighbour (if applicable).
Think of it like this: in the first pass, you walk left-to-right, only looking backwards. If you have a higher rating than the person behind you, you need at least one more candy than them. In the second pass, you walk right-to-left, only looking backwards in that direction. This time, if you have a higher rating than the person to your right, you need at least one more candy than them — but you also can't lose any candies you already earned from the first pass.
By handling each direction independently and taking the maximum, we guarantee that both constraints are satisfied simultaneously.
approach: |
We solve this using a **Two-Pass Greedy Approach**:
**Step 1: Initialise the candies array**
- Create an array `candies` of length `n`, initialised with `1` for each child
- This satisfies the first constraint: every child gets at least one candy
&nbsp;
**Step 2: Left-to-right pass**
- Iterate from index `1` to `n-1`
- For each child, if `ratings[i] > ratings[i-1]`, they must have more candies than their left neighbour
- Set `candies[i] = candies[i-1] + 1` in this case
&nbsp;
**Step 3: Right-to-left pass**
- Iterate from index `n-2` down to `0`
- For each child, if `ratings[i] > ratings[i+1]`, they must have more candies than their right neighbour
- Set `candies[i] = max(candies[i], candies[i+1] + 1)` — we use `max` to preserve any larger value from the left-to-right pass
&nbsp;
**Step 4: Return the total**
- Sum all values in `candies` to get the minimum total candies needed
&nbsp;
This greedy approach works because each pass independently satisfies one direction of the constraint, and taking the maximum at each position ensures both constraints hold.
common_pitfalls:
- title: Single Pass Approach
description: |
A common mistake is trying to solve this in a single pass by looking at both neighbours simultaneously. This fails because when you're at position `i`, you don't yet know the final candy count for position `i+1`.
For example, with `ratings = [1, 2, 3, 2, 1]`, a single left-to-right pass gives `[1, 2, 3, 1, 1]`, which violates the constraint at index 2 (rating 3 should have more candies than its right neighbour with rating 2).
wrong_approach: "Single pass checking both neighbours"
correct_approach: "Two separate passes, one for each direction"
- title: Forgetting to Take Maximum in Second Pass
description: |
In the right-to-left pass, simply setting `candies[i] = candies[i+1] + 1` can overwrite a larger value from the first pass, violating the left neighbour constraint.
For example, with `ratings = [1, 3, 2, 1]`, after the left pass: `[1, 2, 1, 1]`. If we just set values in the right pass, we'd get `[1, 3, 2, 1]`, but position 1 (rating 3) now has fewer candies than needed for its left neighbour constraint. Using `max()` gives `[1, 3, 2, 1]` correctly.
wrong_approach: "Overwriting values in the second pass"
correct_approach: "Use max(current, neighbour + 1) to preserve larger values"
- title: Equal Ratings Confusion
description: |
Children with **equal** ratings don't need to have the same number of candies. The constraint only requires that children with *higher* ratings get more candies than their neighbours.
For `ratings = [1, 2, 2]`, the valid answer is `[1, 2, 1]` with a total of 4 candies. The two children with rating 2 can have different candy counts because neither is *higher* than the other.
key_takeaways:
- "**Two-pass pattern**: When a decision depends on both left and right context, process each direction independently and combine results"
- "**Greedy with local constraints**: Satisfy constraints locally at each step — the combination of both passes guarantees global correctness"
- "**Initialise with minimum valid state**: Starting with 1 candy per child ensures the baseline constraint is met before optimising"
- "**Related problems**: This two-pass technique appears in problems like Trapping Rain Water, Product of Array Except Self, and other scenarios where each element depends on both neighbours"
time_complexity: "O(n). We make two passes through the array, each taking O(n) time."
space_complexity: "O(n). We use an auxiliary array of size `n` to store the candy count for each child."
solutions:
- approach_name: Two-Pass Greedy
is_optimal: true
code: |
def candy(ratings: list[int]) -> int:
n = len(ratings)
# Start with 1 candy per child (minimum requirement)
candies = [1] * n
# Left to right: ensure higher rating than left neighbour
# gets more candies
for i in range(1, n):
if ratings[i] > ratings[i - 1]:
candies[i] = candies[i - 1] + 1
# Right to left: ensure higher rating than right neighbour
# gets more candies (keep max to preserve left constraint)
for i in range(n - 2, -1, -1):
if ratings[i] > ratings[i + 1]:
candies[i] = max(candies[i], candies[i + 1] + 1)
# Total candies needed
return sum(candies)
explanation: |
**Time Complexity:** O(n) — Two linear passes through the array.
**Space Complexity:** O(n) — Auxiliary array to store candy counts.
The two-pass approach decouples the left and right neighbour constraints, making each pass simple: just compare with the previous element in the direction of traversal. Taking the maximum in the second pass ensures we don't violate constraints established in the first pass.
- approach_name: Single Array with Two Passes (Space Optimised Variant)
is_optimal: false
code: |
def candy(ratings: list[int]) -> int:
n = len(ratings)
if n == 1:
return 1
candies = [1] * n
# Forward pass
for i in range(1, n):
if ratings[i] > ratings[i - 1]:
candies[i] = candies[i - 1] + 1
# Backward pass with running sum
total = candies[n - 1]
for i in range(n - 2, -1, -1):
if ratings[i] > ratings[i + 1]:
candies[i] = max(candies[i], candies[i + 1] + 1)
total += candies[i]
return total
explanation: |
**Time Complexity:** O(n) — Two linear passes.
**Space Complexity:** O(n) — Same as above, but avoids a separate sum() call.
This variant computes the sum during the second pass rather than calling `sum()` at the end. The space complexity remains O(n) due to the candies array, but it's slightly more efficient in practice by avoiding an extra iteration.
- approach_name: Brute Force (Iterative Fixing)
is_optimal: false
code: |
def candy(ratings: list[int]) -> int:
n = len(ratings)
candies = [1] * n
changed = True
# Keep iterating until no changes needed
while changed:
changed = False
for i in range(n):
# Check left neighbour
if i > 0 and ratings[i] > ratings[i - 1]:
if candies[i] <= candies[i - 1]:
candies[i] = candies[i - 1] + 1
changed = True
# Check right neighbour
if i < n - 1 and ratings[i] > ratings[i + 1]:
if candies[i] <= candies[i + 1]:
candies[i] = candies[i + 1] + 1
changed = True
return sum(candies)
explanation: |
**Time Complexity:** O(n^2) — In the worst case (strictly decreasing ratings), we need O(n) passes, each taking O(n) time.
**Space Complexity:** O(n) — Candy array storage.
This approach repeatedly scans the array, fixing any violations until no more changes are needed. While correct, it's inefficient and can cause TLE on large inputs. It's included to illustrate why the two-pass greedy approach is superior.