LEETCODE 338Easy

比特位计数

i 右移一位只丢掉最低位,所以 bits[i] = bits[i >> 1] + (i & 1),答案表自己递推自己。

问题拆解

0n 的每个整数,求其二进制表示中 1 的个数,返回长度为 n + 1 的数组。

逐个数字调用 Brian Kernighan 或者内置的 popcount 当然能过,时间是 O(n log n)。但题目进阶要求一次遍历 O(n),这是在提示:这 n + 1 个答案之间有关系,不该各算各的。关系在哪?看 ii >> 1:右移一位丢掉的只是最低位,剩下的高位部分与 i >> 1 一模一样。所以 i 的 1 的个数,等于 i >> 1 的 1 的个数,再加上被丢掉的那个最低位是不是 1——也就是 i & 1

求一批数的答案时,先问“大数的答案能不能由小数的答案拼出来”——i >> 1 < i 恒成立,递推天然无后效性。

用已算出的答案递推

bits[0] = 0,从 1 开始正序填表,每个 bits[i] 只查一次表、做一次加法。

public int[] countBits(int n) {
    int[] bits = new int[n + 1];
    for (int i = 1; i <= n; i++) {
        bits[i] = bits[i >> 1] + (i & 1); // 高位部分 + 最低位
    }
    return bits;
}
def countBits(n: int) -> List[int]:
    bits = [0] * (n + 1)
    for i in range(1, n + 1):
        bits[i] = bits[i >> 1] + (i & 1)  # 高位部分 + 最低位
    return bits
func countBits(n int) []int {
    bits := make([]int, n+1)
    for i := 1; i <= n; i++ {
        bits[i] = bits[i>>1] + i&1 // 高位部分 + 最低位
    }
    return bits
}
pub fn count_bits(n: i32) -> Vec<i32> {
    let n = n as usize;
    let mut bits = vec![0; n + 1];
    for i in 1..=n {
        bits[i] = bits[i >> 1] + (i & 1) as i32; // 高位部分 + 最低位
    }
    bits
}

正序填表是安全的,因为 i >> 1 严格小于 ii ≥ 1 时),查表时那一格一定已经算好。Java 里要留意运算符优先级:bits[i >> 1] + i & 1 会先加后与,必须给 (i & 1) 加括号;Go 的 & 优先级高于 +,反而可以不加,这类跨语言的优先级差异值得在写位运算时多看一眼。

另一条等价的递推是 bits[i] = bits[i & (i - 1)] + 1i & (i - 1)i 最低的一个 1 清零,得到的数同样比 i 小且已算好,个数恰好少 1。按最低位拆和按最低的 1 拆,殊途同归。

复杂度

指标 复杂度 原因
时间 O(n) 每个数只做一次查表和加法
空间 O(1) 除返回的答案数组外无额外空间

可以迁移的模式

  • 对一段连续整数批量求某种位属性,先找 ii >> 1i & (i - 1) 这类“更小的相关数”,让答案表自己递推自己;
  • x & (x - 1) 清除最低位的 1、x & 1 取最低位,是位运算里出场率最高的两个原子操作;
  • DP 的状态不一定来自题面定义,数值本身的二进制结构也能充当子问题。

把“每个数独立计算”换成“答案之间互相引用”,正是这题从 O(n log n)O(n) 的全部秘密。