181 lines
7.8 KiB
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`
|
|
|
|
|
|
|
|
**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
|
|
|
|
|
|
|
|
**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)
|
|
|
|
|
|
|
|
**Step 4: Return the total**
|
|
|
|
- Return the accumulated operation count
|
|
|
|
|
|
|
|
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.
|