questions C

This commit is contained in:
2025-05-25 10:16:13 +01:00
parent 2123791ec3
commit e028167a47
85 changed files with 16925 additions and 0 deletions

View File

@@ -0,0 +1,181 @@
title: Counting Bits
slug: counting-bits
difficulty: easy
leetcode_id: 338
leetcode_url: https://leetcode.com/problems/counting-bits/
categories:
- arrays
- dynamic-programming
- math
patterns:
- dynamic-programming
function_signature: "def count_bits(n: int) -> list[int]:"
test_cases:
visible:
- input: { n: 2 }
expected: [0, 1, 1]
- input: { n: 5 }
expected: [0, 1, 1, 2, 1, 2]
- input: { n: 0 }
expected: [0]
hidden:
- input: { n: 1 }
expected: [0, 1]
- input: { n: 7 }
expected: [0, 1, 1, 2, 1, 2, 2, 3]
- input: { n: 10 }
expected: [0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2]
description: |
Given an integer `n`, return an array `ans` of length `n + 1` such that for each `i` (`0 <= i <= n`), `ans[i]` is the **number of `1`'s** in the binary representation of `i`.
constraints: |
- `0 <= n <= 10^5`
examples:
- input: "n = 2"
output: "[0, 1, 1]"
explanation: "0 → 0 (zero 1's), 1 → 1 (one 1), 2 → 10 (one 1)"
- input: "n = 5"
output: "[0, 1, 1, 2, 1, 2]"
explanation: "0 → 0, 1 → 1, 2 → 10, 3 → 11, 4 → 100, 5 → 101"
explanation:
intuition: |
Imagine you're counting the number of `1` bits in binary for every number from `0` to `n`. The naive approach would be to convert each number to binary and count — but there's a beautiful pattern hiding in the numbers themselves.
Consider the relationship between a number and its half. When you divide a number by 2 (right shift), you simply remove the last bit. For example:
- `5` in binary is `101` (two 1's)
- `5 >> 1 = 2` in binary is `10` (one 1)
The only difference is whether the **last bit** (least significant bit) was a `1` or `0`. So if you already know how many 1's are in `n // 2`, you just need to check if `n` is odd (has a `1` in the last position).
This gives us the recurrence: `countBits(i) = countBits(i >> 1) + (i & 1)`
Think of it like building up your answers: once you know the bit count for smaller numbers, you can instantly compute it for larger ones by leveraging the relationship between a number and its right-shifted version.
approach: |
We solve this using **Dynamic Programming** with a bit manipulation insight:
**Step 1: Create the result array**
- `ans`: Array of size `n + 1` initialised with zeros
- `ans[0] = 0` since zero has no `1` bits (base case)
&nbsp;
**Step 2: Build up using the recurrence relation**
- For each `i` from `1` to `n`:
- `ans[i] = ans[i >> 1] + (i & 1)`
- `i >> 1`: Right shift gives us the number with the last bit removed
- `i & 1`: Checks if the current number is odd (last bit is `1`)
&nbsp;
**Step 3: Return the result**
- Return `ans` containing the bit count for every number from `0` to `n`
&nbsp;
The key insight is that `i >> 1` is always less than `i` (for `i > 0`), so we've already computed its answer. This allows us to solve the problem in a single pass with O(1) work per number.
common_pitfalls:
- title: Using Built-in Popcount for Each Number
description: |
A common first approach is to use `bin(i).count('1')` or `__builtin_popcount(i)` for each number from `0` to `n`.
While this works, it has **O(log i)** time per number (since each number has up to `log(i)` bits), giving overall **O(n log n)** time complexity.
The problem specifically asks for an O(n) solution, which requires recognising the DP pattern.
wrong_approach: "Loop with bin(i).count('1') for each i"
correct_approach: "Use DP recurrence: ans[i] = ans[i >> 1] + (i & 1)"
- title: Missing the Bit Shift Relationship
description: |
It's tempting to look for patterns like "powers of 2" or "consecutive numbers", but these don't lead to an elegant O(n) solution.
The key insight is that `i` and `i >> 1` differ by exactly one bit operation. Every bit in `i >> 1` was already in `i` (just shifted right), and we only need to account for the rightmost bit we "lost".
For example: `6 = 110` and `6 >> 1 = 3 = 11`. The bit counts are 2 and 2 respectively. Adding `6 & 1 = 0` gives us `2 + 0 = 2`. ✓
wrong_approach: "Looking for complex mathematical patterns"
correct_approach: "Recognise i >> 1 has same bits minus the LSB"
- title: Off-by-One in Array Size
description: |
The problem asks for numbers `0` through `n` inclusive, which means `n + 1` total numbers.
Creating an array of size `n` instead of `n + 1` will miss the last element or cause an index error.
wrong_approach: "ans = [0] * n"
correct_approach: "ans = [0] * (n + 1)"
key_takeaways:
- "**Bit manipulation + DP**: Combining bit operations with dynamic programming often reveals elegant solutions"
- "**Right shift insight**: `i >> 1` removes the last bit, so `countBits(i) = countBits(i >> 1) + (i & 1)`"
- "**O(n) vs O(n log n)**: Recognising subproblem relationships can reduce complexity from O(n log n) to O(n)"
- "**Build from smaller to larger**: Classic DP pattern — use already-computed answers for smaller inputs"
time_complexity: "O(n). We compute the answer for each number from `0` to `n` in constant time using the recurrence."
space_complexity: "O(n). We store `n + 1` values in the result array (required by the problem output)."
solutions:
- approach_name: Dynamic Programming with Bit Shift
is_optimal: true
code: |
def count_bits(n: int) -> list[int]:
# Result array: ans[i] will hold count of 1's in binary of i
ans = [0] * (n + 1)
for i in range(1, n + 1):
# i >> 1 removes last bit, i & 1 checks if last bit is 1
# Since i >> 1 < i, we've already computed ans[i >> 1]
ans[i] = ans[i >> 1] + (i & 1)
return ans
explanation: |
**Time Complexity:** O(n) — Single pass from 1 to n with O(1) work each.
**Space Complexity:** O(n) — The output array of size n + 1.
This solution uses the recurrence `ans[i] = ans[i >> 1] + (i & 1)`. Right-shifting removes the last bit, and `i & 1` tells us if that bit was a `1`. Since we process numbers in order, `i >> 1` is always already computed.
- approach_name: Dynamic Programming with Last Set Bit
is_optimal: true
code: |
def count_bits(n: int) -> list[int]:
# Result array initialised with zeros
ans = [0] * (n + 1)
for i in range(1, n + 1):
# i & (i - 1) removes the rightmost set bit
# So we add 1 to the count of the number without that bit
ans[i] = ans[i & (i - 1)] + 1
return ans
explanation: |
**Time Complexity:** O(n) — Single pass with O(1) work per number.
**Space Complexity:** O(n) — The output array.
Alternative DP approach using `i & (i - 1)`, which clears the rightmost `1` bit. For example, `6 & 5 = 110 & 101 = 100 = 4`. Since `i & (i - 1) < i`, we've already computed its bit count, and we just add `1` for the bit we removed.
- approach_name: Built-in Popcount
is_optimal: false
code: |
def count_bits(n: int) -> list[int]:
ans = []
for i in range(n + 1):
# Convert to binary string and count '1' characters
ans.append(bin(i).count('1'))
return ans
explanation: |
**Time Complexity:** O(n log n) — For each number, counting bits takes O(log i) time.
**Space Complexity:** O(n) — The output array.
This straightforward approach uses Python's built-in functions but doesn't achieve the O(n) time requested in the follow-up. Useful for understanding the problem but not optimal.