题目描述
思路解析
一句话答案:LeetCode 338 比特位计数的最优解是一维动态规划:dp[i] = dp[i >> 1] + (i & 1)。i 右移一位(去掉最低位)后的 1 的个数早已算好,补上最低位是否为 1 即可,每个数 O(1) 转移,整体时间 O(n);除必须输出的答案数组外只用常数空间。
比特位计数在问什么
给一个整数 n,要求返回长度为 n+1 的数组,第 i 个元素是 i 的二进制表示里 1 的个数(0 ≤ i ≤ n)。比如 n = 10 时答案是 [0,1,1,2,1,2,2,3,1,2,2]。单独数一个数的 1 谁都会,这道题的考点是:要一口气数 0 到 n 全部的数,能不能让后面的数「白拿」前面已经算好的结果。
为什么逐个数各自数一遍还不够好
直觉解是对每个 i 独立做一次位计数(逐位右移或 Kernighan 消 1),每个数要 O(log i) 步,总共 O(n log n)。慢的根源是重复劳动:数 6(二进制 110)的时候,其实把 3(二进制 11)的 1 又数了一遍——6 的高位部分就是 3。
关键观察由此浮出来:任何数 i 右移一位(i >> 1,即抹掉最低位)得到的那个数比 i 小,它的答案在从小到大填表时早就算好了。i 和 i >> 1 的二进制只差最低那一位,这一位是不是 1,用 i & 1 一步就能测出来。既然大问题的答案等于「已解决的小问题 + 常数补充」,动态规划的结构就齐了。
dp 数组的定义和转移方程为什么成立
定义 dp[i] = 数 i 的二进制中 1 的个数,下标直接就是数值本身,dp[0] = 0 是地基。转移方程 dp[i] = dp[i >> 1] + (i & 1):i 的全部 1 分成两部分——高位部分(就是 i >> 1 的二进制)的 1,加上最低位可能贡献的那一个。偶数最低位是 0,答案与 dp[i >> 1] 相同;奇数最低位是 1,再加一。
正确性的依据是无后效性:i >> 1 严格小于 i(i ≥ 1 时),按 i 从 1 到 n 顺序填表,转移用到的那格永远已经填好,绝无循环依赖。拿 10 验证:10 是 1010,10 >> 1 = 5(101),dp[5] = 2,10 是偶数不加,dp[10] = 2,正确。
另一种转移 dp[i & (i-1)] + 1 是什么原理
同样 O(n) 的还有一种写法:dp[i] = dp[i & (i - 1)] + 1。i & (i - 1) 会精确抹掉 i 最低位的那个 1,所以它的 1 比 i 恰好少一个,且这个数也比 i 小、已经算好。两种转移都体现同一个思想——从 i 身上剥掉一小块位结构,剩下的部分复用已有答案。面试里能随手写出任意一种并解释另一种,位运算功底就立住了。
复杂度与两个容易踩的坑
时间 O(n):n+1 个数每个只做一次右移、一次按位与、一次加法,全是 O(1)。空间上答案数组本身 O(n) 是题目要求的输出,额外空间只有循环变量,O(1)。
两个高频错误:一是把转移里的 i >> 1 顺手写成 i - 1——这道题依赖的是「去掉最低位」的位结构关系,不是「前一个数」的算术关系,i - 1 的 1 的个数和 i 没有固定加减规律;二是数组长度开成 n,忘了答案要覆盖 0 到 n 共 n+1 个数,最后一格越界或缺失。
▶ 动画逐步走查(文字版)——想跟着上方动画一帧帧对照就展开
记住这条转移式:去掉最低位(右移一位)的答案早算好了,补上最低位是不是 1 就行。下面每格都在套它。
列头是下标 i(0 到 10),下行 dp[i] 待填。从最左 dp[0] 开始,维度全程不变。
0 的二进制就是 0,一个 1 都没有:dp[0]=0。这是地基。
算 dp[1]:1 的二进制是 1,去掉最低位变成 0(=0,就是 1>>1)。蓝格 dp[0]=0 早填好了。
1 是奇数,最低位是 1,给 dp[0] 再 +1,得 1 填进 dp[1]。
算 dp[2]:2 的二进制是 10,去掉最低位变成 1(=1,就是 2>>1)。蓝格 dp[1]=1 早填好了。
2 是偶数,最低位是 0,跟 dp[1] 一样多,得 1 填进 dp[2]。
算 dp[3]:3 的二进制是 11,去掉最低位变成 1(=1,就是 3>>1)。蓝格 dp[1]=1 早填好了。
3 是奇数,最低位是 1,给 dp[1] 再 +1,得 2 填进 dp[3]。
算 dp[4]:4 的二进制是 100,去掉最低位变成 10(=2,就是 4>>1)。蓝格 dp[2]=1 早填好了。
4 是偶数,最低位是 0,跟 dp[2] 一样多,得 1 填进 dp[4]。
算 dp[5]:5 的二进制是 101,去掉最低位变成 10(=2,就是 5>>1)。蓝格 dp[2]=1 早填好了。
5 是奇数,最低位是 1,给 dp[2] 再 +1,得 2 填进 dp[5]。
算 dp[6]:6 的二进制是 110,去掉最低位变成 11(=3,就是 6>>1)。蓝格 dp[3]=2 早填好了。
6 是偶数,最低位是 0,跟 dp[3] 一样多,得 2 填进 dp[6]。
算 dp[7]:7 的二进制是 111,去掉最低位变成 11(=3,就是 7>>1)。蓝格 dp[3]=2 早填好了。
7 是奇数,最低位是 1,给 dp[3] 再 +1,得 3 填进 dp[7]。
算 dp[8]:8 的二进制是 1000,去掉最低位变成 100(=4,就是 8>>1)。蓝格 dp[4]=1 早填好了。
8 是偶数,最低位是 0,跟 dp[4] 一样多,得 1 填进 dp[8]。
算 dp[9]:9 的二进制是 1001,去掉最低位变成 100(=4,就是 9>>1)。蓝格 dp[4]=1 早填好了。
9 是奇数,最低位是 1,给 dp[4] 再 +1,得 2 填进 dp[9]。
算 dp[10]:10 的二进制是 1010,去掉最低位变成 101(=5,就是 10>>1)。蓝格 dp[5]=2 早填好了。
10 是偶数,最低位是 0,跟 dp[5] 一样多,得 2 填进 dp[10]。
整行 dp 就是答案:[0,1,1,2,1,2,2,3,1,2,2]。每格只看了它「右移一位」那格,所以总共只扫一遍。
边界先想清。
两个高频追问。
参考代码
def countBits(n): dp = [0] * (n + 1) for i in range(1, n + 1): dp[i] = dp[i >> 1] + (i & 1) return dp复杂度
- 时间:O(n),每个数 O(1) 转移,一遍扫完
- 空间:O(1),除答案数组外只用常数;答案本身 O(n)
易错点
面试追问把动画讲成自己的话
追问为什么用 i>>1 而不是 i 的其他子结构?
追问还有别的 O(n) 写法吗?
这道题到这就讲完了。动画和文字是同一套思路——别光看,关掉页面自己默写一遍。然后顺着主线继续:
颠倒二进制位
LeetCode 190 · 简单 · 沿着 位运算 继续往下推进
把这道题真正学会,再走
图解算法年卡 ¥99 /年
- ✓本题每步动画的吴师兄语音讲解(全站陆续覆盖)
- ✓小欧带 8 步通关训练——追问到不看答案也能写对、能 30 秒讲给面试官听
- ✓学习报告,记录每道题的掌握程度
76k+ GitHub Star · 吴师兄开源图解算法,几十万开发者在看的算法讲解
想成体系刷透这类套路?去图解算法专题