Description

給定整數 n,回傳長度為 n + 1 的陣列 ans,其中 ans[i]i 的二進位表示中 1 的個數。要求 O(n) 時間複雜度。

Example:

Input: n = 5 Output: [0,1,1,2,1,2]

Intuition#

核心思路:利用 DP 關係 dp[i] = dp[i » 1] + (i & 1),右移一位的結果已經算過。

  • 暴力法對每個數字分別算 popcount,時間 O(n log n)
  • 觀察規律:數字 i 的 1 的個數 = i 右移一位的 1 的個數 + i 的最低位
  • 另一個 DP 關係:dp[i] = dp[i & (i - 1)] + 1(消除最低位的 1)

Approaches#

1: 逐一計算 Popcount#

  • 概念: 對 0 到 n 的每個數字,分別用 Brian Kernighan 演算法計算 1 的個數
  • 時間複雜度: O(n log n) - 每個數字最多 log n 個 bit
  • 空間複雜度: O(1) - 不含輸出陣列
class Solution {
    fun countBits(n: Int): IntArray {
        val ans = IntArray(n + 1)
        for (i in 0..n) {
            var count = 0
            var num = i
            while (num != 0) {
                num = num and (num - 1)
                count++
            }
            ans[i] = count
        }
        return ans
    }
}

⭐2: DP + 右移#

  • 概念: 數字 i 的 1 的個數等於 i » 1 的 1 的個數加上 i 的最低位(i & 1)
  • 時間複雜度: O(n) - 每個數字只需 O(1)
  • 空間複雜度: O(1) - 不含輸出陣列
class Solution {
    fun countBits(n: Int): IntArray {
        val ans = IntArray(n + 1)
        for (i in 1..n) {
            ans[i] = ans[i shr 1] + (i and 1)
        }
        return ans
    }
}
補充:DP + 消除最低位的 1

利用 i & (i - 1) 消除最低位的 1,dp[i] = dp[i & (i - 1)] + 1。

class Solution {
    fun countBits(n: Int): IntArray {
        val ans = IntArray(n + 1)
        for (i in 1..n) {
            ans[i] = ans[i and (i - 1)] + 1
        }
        return ans
    }
}

這個方法本質上是利用已計算過的結果(消除一個 1 後的數字),加上被消除的那個 1。

🔑 Takeaways#

  • Pattern: 位元操作 + DP,利用位元結構找出子問題關係
  • 關鍵技巧: i » 1 和 i & (i - 1) 都可以將問題縮小到已計算過的子問題,是位元 DP 的常見模式