Files
codetutor/backend/data/questions/broken-calculator.yaml

181 lines
7.8 KiB
YAML

title: Broken Calculator
slug: broken-calculator
difficulty: medium
leetcode_id: 991
leetcode_url: https://leetcode.com/problems/broken-calculator/
categories:
- math
patterns:
- greedy
description: |
There is a broken calculator that has the integer `startValue` on its display initially. In one operation, you can:
- Multiply the number on display by `2`, or
- Subtract `1` from the number on display.
Given two integers `startValue` and `target`, return *the minimum number of operations needed to display* `target` *on the calculator*.
constraints: |
- `1 <= startValue, target <= 10^9`
examples:
- input: "startValue = 2, target = 3"
output: "2"
explanation: "Use double operation and then decrement operation {2 -> 4 -> 3}."
- input: "startValue = 5, target = 8"
output: "2"
explanation: "Use decrement and then double {5 -> 4 -> 8}."
- input: "startValue = 3, target = 10"
output: "3"
explanation: "Use double, decrement and double {3 -> 6 -> 5 -> 10}."
explanation:
intuition: |
The forward direction (from `startValue` to `target`) involves choices that are hard to evaluate: should you double now or subtract first? The decision tree branches exponentially.
The key insight is to **think backwards**: work from `target` back to `startValue`. In reverse, the operations become:
- Divide by `2` (inverse of multiply)
- Add `1` (inverse of subtract)
Why does this help? Because now there's a **greedy rule**: if `target` is even, we *must* divide by 2 (it's always better than adding 1 multiple times). If `target` is odd, we have no choice but to add 1 first to make it even.
Think of it like this: imagine you're trying to reduce a number to a smaller one. Dividing by 2 is a powerful operation that halves the distance quickly. Adding 1 is weak but necessary when division isn't possible. By always choosing division when available, we minimise the total operations.
approach: |
We solve this using a **Reverse Greedy Approach**:
**Step 1: Handle the trivial case**
- If `startValue >= target`, we can only subtract, so return `startValue - target`
&nbsp;
**Step 2: Work backwards from target**
- While `target > startValue`:
- If `target` is even: divide by 2 (one operation)
- If `target` is odd: add 1 to make it even (one operation)
- Increment the operation counter
&nbsp;
**Step 3: Add remaining difference**
- After the loop, `target <= startValue`
- Add `startValue - target` to the operation count (these are the subtractions needed in the forward direction, or additions in reverse)
&nbsp;
**Step 4: Return the total**
- Return the accumulated operation count
&nbsp;
This greedy approach is optimal because dividing by 2 is always the most efficient way to reduce a number, and we only add 1 when forced to by odd numbers.
common_pitfalls:
- title: Forward Simulation Trap
description: |
A tempting approach is to simulate forward from `startValue` to `target`, using BFS or recursion to explore all possible paths. This leads to exponential time complexity.
With constraints up to `10^9`, this approach will cause **Time Limit Exceeded** or **Memory Limit Exceeded**. The state space is too large to explore exhaustively.
wrong_approach: "BFS or recursion exploring all forward paths"
correct_approach: "Work backwards with greedy decisions"
- title: Forgetting the Base Case
description: |
When `startValue >= target`, you might try to apply the backward algorithm, but division doesn't help when you're already above the target.
For example, `startValue = 10, target = 5`: the answer is simply `10 - 5 = 5` subtractions. No multiplication/division is useful here because multiplying only takes you further from the target.
wrong_approach: "Applying the same logic regardless of start vs target"
correct_approach: "Handle startValue >= target as a special case"
- title: Integer Overflow Concerns
description: |
When working backwards, you only divide and add 1, so values decrease or increase by 1. There's no overflow risk.
However, if you tried a forward approach with multiplication, values could overflow quickly. This is another reason the backward approach is superior.
key_takeaways:
- "**Reverse thinking**: When forward decisions are complex, try working backwards where the choices may be clearer"
- "**Greedy with proof**: The greedy choice (always divide when even) is provably optimal because division is strictly more powerful than addition"
- "**Constraint awareness**: With `10^9` constraints, any exponential or even O(n) simulation would be too slow; we need O(log n)"
- "**Related problems**: This pattern of reversing operations appears in problems like *Reaching Points* and other math/greedy puzzles"
time_complexity: "O(log(target)). Each division halves the target, so we perform at most O(log(target)) divisions. The additions between divisions are bounded."
space_complexity: "O(1). We only use a few integer variables regardless of input size."
solutions:
- approach_name: Reverse Greedy
is_optimal: true
code: |
def broken_calc(start_value: int, target: int) -> int:
# If we're already at or above target, just subtract
if start_value >= target:
return start_value - target
operations = 0
# Work backwards from target to start_value
while target > start_value:
if target % 2 == 0:
# Even: divide by 2 (reverse of multiply)
target //= 2
else:
# Odd: add 1 (reverse of subtract)
target += 1
operations += 1
# Add the remaining difference (subtractions in forward direction)
return operations + (start_value - target)
explanation: |
**Time Complexity:** O(log(target)) — Each division halves the value, giving logarithmic iterations.
**Space Complexity:** O(1) — Only a few integer variables used.
By working backwards, we transform a complex decision problem into a simple greedy one. When target is even, dividing is always optimal. When odd, we must add 1 first. This guarantees the minimum operations.
- approach_name: BFS (Suboptimal)
is_optimal: false
code: |
from collections import deque
def broken_calc(start_value: int, target: int) -> int:
# BFS from start_value to target
# WARNING: This is too slow for large inputs!
if start_value >= target:
return start_value - target
queue = deque([(start_value, 0)]) # (current_value, operations)
visited = {start_value}
while queue:
current, ops = queue.popleft()
# Try multiply by 2
doubled = current * 2
if doubled == target:
return ops + 1
if doubled < target * 2 and doubled not in visited:
visited.add(doubled)
queue.append((doubled, ops + 1))
# Try subtract 1
decremented = current - 1
if decremented == target:
return ops + 1
if decremented > 0 and decremented not in visited:
visited.add(decremented)
queue.append((decremented, ops + 1))
return -1 # Should never reach here
explanation: |
**Time Complexity:** O(2^n) in worst case — Exponential state space exploration.
**Space Complexity:** O(2^n) — Storing visited states.
This BFS approach explores all possible paths forward. While conceptually correct, it's far too slow for the given constraints (`target` up to `10^9`). Included to illustrate why the greedy reverse approach is necessary.