182 lines
7.6 KiB
YAML
182 lines
7.6 KiB
YAML
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)
|
|
|
|
|
|
|
|
**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`)
|
|
|
|
|
|
|
|
**Step 3: Return the result**
|
|
|
|
- Return `ans` containing the bit count for every number from `0` to `n`
|
|
|
|
|
|
|
|
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.
|