questions F-L
This commit is contained in:
195
backend/data/questions/happy-number.yaml
Normal file
195
backend/data/questions/happy-number.yaml
Normal file
@@ -0,0 +1,195 @@
|
||||
title: Happy Number
|
||||
slug: happy-number
|
||||
difficulty: easy
|
||||
leetcode_id: 202
|
||||
leetcode_url: https://leetcode.com/problems/happy-number/
|
||||
categories:
|
||||
- math
|
||||
- hash-tables
|
||||
patterns:
|
||||
- fast-slow-pointers
|
||||
|
||||
function_signature: "def is_happy(n: int) -> bool:"
|
||||
|
||||
test_cases:
|
||||
visible:
|
||||
- input: { n: 19 }
|
||||
expected: true
|
||||
- input: { n: 2 }
|
||||
expected: false
|
||||
- input: { n: 1 }
|
||||
expected: true
|
||||
hidden:
|
||||
- input: { n: 7 }
|
||||
expected: true
|
||||
- input: { n: 4 }
|
||||
expected: false
|
||||
- input: { n: 100 }
|
||||
expected: true
|
||||
|
||||
description: |
|
||||
Write an algorithm to determine if a number `n` is happy.
|
||||
|
||||
A **happy number** is a number defined by the following process:
|
||||
|
||||
- Starting with any positive integer, replace the number by the sum of the squares of its digits.
|
||||
- Repeat the process until the number equals `1` (where it will stay), or it **loops endlessly in a cycle** which does not include `1`.
|
||||
- Those numbers for which this process **ends in 1** are happy.
|
||||
|
||||
Return `true` if `n` is a happy number, and `false` if not.
|
||||
|
||||
constraints: |
|
||||
- `1 <= n <= 2^31 - 1`
|
||||
|
||||
examples:
|
||||
- input: "n = 19"
|
||||
output: "true"
|
||||
explanation: "1^2 + 9^2 = 82 -> 8^2 + 2^2 = 68 -> 6^2 + 8^2 = 100 -> 1^2 + 0^2 + 0^2 = 1"
|
||||
- input: "n = 2"
|
||||
output: "false"
|
||||
explanation: "The sequence 2 -> 4 -> 16 -> 37 -> 58 -> 89 -> 145 -> 42 -> 20 -> 4 enters a cycle that never reaches 1."
|
||||
|
||||
explanation:
|
||||
intuition: |
|
||||
Think of this problem as following a path through a maze of numbers. Starting from `n`, you compute the sum of squared digits to get the next number, then repeat. The key insight is that this sequence must eventually do one of two things: either reach `1` (happy!) or enter a cycle (unhappy).
|
||||
|
||||
Why must it cycle? Because the sum of squared digits for any number has an upper bound. For a number with `d` digits, the maximum sum is `d * 81` (when all digits are `9`). For the largest input (`2^31 - 1`, which has 10 digits), the maximum possible sum is 810. So after at most one step, you're working with numbers in a bounded range, and a bounded sequence that never terminates must eventually repeat.
|
||||
|
||||
This cycle-detection insight opens up two elegant solutions:
|
||||
|
||||
1. **Hash Set**: Track every number you've seen. If you see a repeat before reaching `1`, there's a cycle.
|
||||
2. **Floyd's Cycle Detection (Fast-Slow Pointers)**: Use two "runners" through the sequence at different speeds. If there's a cycle, the fast runner will eventually lap the slow runner.
|
||||
|
||||
The fast-slow pointer approach is particularly elegant because it uses O(1) space instead of O(n) for storing visited numbers.
|
||||
|
||||
approach: |
|
||||
We solve this using **Floyd's Cycle Detection** (also known as the tortoise and hare algorithm):
|
||||
|
||||
**Step 1: Define a helper function**
|
||||
|
||||
- `get_next(n)`: Computes the sum of squares of digits
|
||||
- Extract each digit using modulo and integer division
|
||||
- Square each digit and accumulate the sum
|
||||
|
||||
|
||||
|
||||
**Step 2: Initialise two pointers**
|
||||
|
||||
- `slow`: Starts at `n`, moves one step at a time
|
||||
- `fast`: Starts at `get_next(n)`, moves two steps at a time
|
||||
|
||||
|
||||
|
||||
**Step 3: Run the cycle detection loop**
|
||||
|
||||
- While `fast != 1` and `fast != slow`:
|
||||
- Move `slow` one step: `slow = get_next(slow)`
|
||||
- Move `fast` two steps: `fast = get_next(get_next(fast))`
|
||||
- If they meet before reaching `1`, there's a cycle (unhappy)
|
||||
- If `fast` reaches `1`, the number is happy
|
||||
|
||||
|
||||
|
||||
**Step 4: Return the result**
|
||||
|
||||
- Return `fast == 1`
|
||||
- If `fast` is `1`, we found happiness; otherwise we detected a cycle
|
||||
|
||||
common_pitfalls:
|
||||
- title: Infinite Loop Without Cycle Detection
|
||||
description: |
|
||||
A naive approach might just keep computing the next number forever:
|
||||
|
||||
```python
|
||||
while n != 1:
|
||||
n = sum_of_squares(n)
|
||||
return True
|
||||
```
|
||||
|
||||
This will never terminate for unhappy numbers like `2`, which cycle endlessly through `2 -> 4 -> 16 -> 37 -> 58 -> 89 -> 145 -> 42 -> 20 -> 4 -> ...`
|
||||
|
||||
You **must** detect cycles, either with a hash set or Floyd's algorithm.
|
||||
wrong_approach: "Loop until n equals 1"
|
||||
correct_approach: "Track visited numbers or use Floyd's cycle detection"
|
||||
|
||||
- title: Forgetting Edge Cases
|
||||
description: |
|
||||
The number `1` is already happy (sum of squares of `1` is `1`). Single-digit numbers like `7` are also happy (`7 -> 49 -> 97 -> 130 -> 10 -> 1`).
|
||||
|
||||
Make sure your initial setup handles these correctly. With Floyd's algorithm, initialising `slow = n` and `fast = get_next(n)` naturally handles `n = 1` because `fast` immediately becomes `1`.
|
||||
|
||||
- title: Integer Overflow in get_next
|
||||
description: |
|
||||
When extracting digits, some implementations might use string conversion which is slower. The mathematical approach using `n % 10` and `n // 10` is both faster and avoids any potential issues with very large numbers during intermediate steps.
|
||||
|
||||
However, since the sum of squared digits is bounded (maximum ~810 for 10-digit numbers), overflow is not a concern for the result.
|
||||
|
||||
key_takeaways:
|
||||
- "**Cycle detection pattern**: Floyd's algorithm (fast-slow pointers) is useful whenever you need to detect cycles in a sequence with O(1) space"
|
||||
- "**Bounded sequences**: Recognising that the sequence values are bounded (max ~810) proves that cycles must occur for non-happy numbers"
|
||||
- "**Math vs Hash Table tradeoff**: The hash set approach is simpler to understand but uses O(k) space where k is the cycle length; Floyd's uses O(1)"
|
||||
- "**Related problems**: This pattern applies to Linked List Cycle, Find the Duplicate Number, and other sequence-based cycle problems"
|
||||
|
||||
time_complexity: "O(log n). The number of digits in n is O(log n), and we process each number in the sequence. The sequence length is bounded by a constant for any starting value."
|
||||
space_complexity: "O(1) for Floyd's algorithm, or O(log n) for the hash set approach (storing visited numbers)."
|
||||
|
||||
solutions:
|
||||
- approach_name: Floyd's Cycle Detection
|
||||
is_optimal: true
|
||||
code: |
|
||||
def is_happy(n: int) -> bool:
|
||||
def get_next(num: int) -> int:
|
||||
"""Calculate sum of squares of digits."""
|
||||
total = 0
|
||||
while num > 0:
|
||||
digit = num % 10 # Extract last digit
|
||||
total += digit * digit # Add its square
|
||||
num //= 10 # Remove last digit
|
||||
return total
|
||||
|
||||
# Floyd's algorithm: slow moves 1 step, fast moves 2 steps
|
||||
slow = n
|
||||
fast = get_next(n)
|
||||
|
||||
# Continue until fast reaches 1 or they meet (cycle detected)
|
||||
while fast != 1 and slow != fast:
|
||||
slow = get_next(slow) # One step
|
||||
fast = get_next(get_next(fast)) # Two steps
|
||||
|
||||
# Happy if we reached 1, unhappy if cycle detected
|
||||
return fast == 1
|
||||
explanation: |
|
||||
**Time Complexity:** O(log n) — Each number has O(log n) digits to process, and the sequence is bounded.
|
||||
|
||||
**Space Complexity:** O(1) — Only uses two pointer variables regardless of input size.
|
||||
|
||||
Floyd's cycle detection elegantly solves the problem: if a cycle exists, the fast pointer will eventually catch up to the slow pointer. If no cycle exists (happy number), fast reaches 1 first.
|
||||
|
||||
- approach_name: Hash Set
|
||||
is_optimal: false
|
||||
code: |
|
||||
def is_happy(n: int) -> bool:
|
||||
def get_next(num: int) -> int:
|
||||
"""Calculate sum of squares of digits."""
|
||||
total = 0
|
||||
while num > 0:
|
||||
digit = num % 10
|
||||
total += digit * digit
|
||||
num //= 10
|
||||
return total
|
||||
|
||||
# Track all numbers we've seen
|
||||
seen = set()
|
||||
|
||||
while n != 1 and n not in seen:
|
||||
seen.add(n) # Mark current number as visited
|
||||
n = get_next(n) # Move to next in sequence
|
||||
|
||||
# Happy if we reached 1, unhappy if we saw a repeat
|
||||
return n == 1
|
||||
explanation: |
|
||||
**Time Complexity:** O(log n) — Same as Floyd's approach.
|
||||
|
||||
**Space Complexity:** O(log n) — Stores visited numbers in the set.
|
||||
|
||||
This approach is more intuitive: just remember what you've seen. If you see a number twice before reaching 1, you're in a cycle. The tradeoff is using extra memory for the set.
|
||||
Reference in New Issue
Block a user