丑数 II 图解题解
这道题到底在问什么
- 输入
- n = 10
- 输出
- 12
最优解:为什么这么做
一句话答案:LeetCode 264 丑数 II 求只含质因子 2、3、5 的第 n 个数。三指针 DP:dp[i]=min(dp[p2]×2,dp[p3]×3,dp[p5]×5),谁命中谁前移,不漏不重,时间 O(n)、空间 O(n)。
丑数 II 到底要找第几个什么数
给一个整数 n,返回第 n 个丑数。丑数是只含质因子 2、3、5 的正整数(质因子=把一个数拆成质数相乘时用到的那些质数),约定 1 是第 1 个。从小到大数,跳过像 7 这种含别的质因子的数,题面 n=10 的第 10 个丑数是 12。难点在按顺序不漏不重地生成。
从 1 逐个试除为什么越试越亏
从 1 往上逐个数,反复除 2、3、5,能除尽就是丑数。可丑数越往后越稀疏,第 100 个已是 1536,中间大量整数被白白试除,n 一大就拖垮时间。更快的是让每个新丑数由已有丑数乘 2、3、5 按序造出来。
为什么新丑数都藏在三指针的候选里
除了 1,每个丑数都能写成某个更小的丑数乘 2、乘 3 或乘 5。把已生成的丑数按序存进 dp 数组,dp[i] 就是第 i 个丑数(dp 即动态规划,算过的存下来往后用),dp[1]=1 当起点。下一个丑数就在「已有丑数乘 2、乘 3、乘 5」的候选里。与其全乘出来排序,不如用三个指针 p2、p3、p5 记着 2、3、5 各乘到哪,起初都指向 dp[1],每步只看 dp[p2]×2、dp[p3]×3、dp[p5]×5。
三指针凭什么不漏也不重地按序生成
每步取最小的候选填进 dp:dp[i]=min(dp[p2]×2,dp[p3]×3,dp[p5]×5),命中的指针前移。不漏:每个丑数迟早被 ×2、×3、×5 各用一次;不重且按序:每步只取当前最小,填出来天然从小到大。
同一个数可能被两条路造出来:6 既是 3×2 也是 2×3,dp[p2]×2 与 dp[p3]×3 会同时等于 6,只填一个,但 p2、p3 都得前移——只移一个,另一个下一步又会算出 6。所以三个判断是独立的 if,谁等于最小值谁前移,可能同时移两三个。
拿 n=10 把 dp 表一格格填到 12
dp[1]=1,三指针都指向它。第 2 项 min(1×2,1×3,1×5)=2,p2 前移;第 3 项 min(2×2,1×3,1×5)=3,p3 前移;第 4 项 min(2×2,2×3,1×5)=4,p2 前移;第 5 项 min(3×2,2×3,1×5)=5,p5 前移。
第 6 项 min(3×2,2×3,2×5)=6,×2 与 ×3 同时命中,只填一次、p2 与 p3 一起前移。往后取最小:第 7 项 min(4×2,3×3,2×5)=8、第 8 项 =9、第 9 项 min(5×2,4×3,2×5)=10(×2 与 ×5 同时命中)、第 10 项 min(6×2,4×3,3×5)=12。整张表 1、2、3、4、5、6、8、9、10、12 正是前 10 个丑数。
复杂度多少,相等和 dp[1] 两处别写错
生成 n 个丑数,每个只做取最小、移指针的常数操作,O(n) 时间;dp 存下前 n 个丑数,空间 O(n)。
两个坑。一是相等时只移一个指针:多个候选同时等于最小值,得把对应指针全部前移,否则下一步会再造出同一个数,用三个独立 if、别写成 if/elif。二是 dp[1]=1 别漏:1 是第 1 个丑数、三指针的共同起点,漏了整列没法往下乘。
▶ 动画逐步走查(共 22 步)——想跟着动画一帧帧对照就展开
- 3三指针的作用是「不重不漏」:每个已生成的丑数都会被 ×2、×3、×5 各用一次,指针保证按顺序逐个用、不重复。
- 4一行 dp 表,第 i 格存第 i 个丑数。三指针 p2、p3、p5 一开始都指向第 1 格。
- 5约定第 1 个丑数就是 1,填进 dp[1]。此时 p2=p3=p5 都指向 dp[1]。
- 6算第 2 个丑数:三个候选——p2 指 dp[1]=1,×2=2;p3 指 dp[1]=1,×3=3;p5 指 dp[1]=1,×5=5。取三者最小。
- 7最小是 2,填进 dp[2]。命中的指针 p2 前移一格。
- 8算第 3 个丑数:三个候选——p2 指 dp[2]=2,×2=4;p3 指 dp[1]=1,×3=3;p5 指 dp[1]=1,×5=5。取三者最小。
- 9最小是 3,填进 dp[3]。命中的指针 p3 前移一格。
- 10算第 4 个丑数:三个候选——p2 指 dp[2]=2,×2=4;p3 指 dp[2]=2,×3=6;p5 指 dp[1]=1,×5=5。取三者最小。
- 11最小是 4,填进 dp[4]。命中的指针 p2 前移一格。
- 12算第 5 个丑数:三个候选——p2 指 dp[3]=3,×2=6;p3 指 dp[2]=2,×3=6;p5 指 dp[1]=1,×5=5。取三者最小。
- 13最小是 5,填进 dp[5]。命中的指针 p5 前移一格。
- 14算第 6 个丑数:三个候选——p2 指 dp[3]=3,×2=6;p3 指 dp[2]=2,×3=6;p5 指 dp[2]=2,×5=10。取三者最小。
- 15最小是 6,填进 dp[6]。命中的指针 p2、p3 各前移一格(p2、p3 同时命中,一起前移——同时被两个指针命中的数只填一次)。
- 16算第 7 个丑数:三个候选——p2 指 dp[4]=4,×2=8;p3 指 dp[3]=3,×3=9;p5 指 dp[2]=2,×5=10。取三者最小。
- 17最小是 8,填进 dp[7]。命中的指针 p2 前移一格。
- 18算第 8 个丑数:三个候选——p2 指 dp[5]=5,×2=10;p3 指 dp[3]=3,×3=9;p5 指 dp[2]=2,×5=10。取三者最小。
- 19最小是 9,填进 dp[8]。命中的指针 p3 前移一格。
- 20算第 9 个丑数:三个候选——p2 指 dp[5]=5,×2=10;p3 指 dp[4]=4,×3=12;p5 指 dp[2]=2,×5=10。取三者最小。
- 21最小是 10,填进 dp[9]。命中的指针 p2、p5 各前移一格(p2、p5 同时命中,一起前移——同时被两个指针命中的数只填一次)。
- 22算第 10 个丑数:三个候选——p2 指 dp[6]=6,×2=12;p3 指 dp[4]=4,×3=12;p5 指 dp[3]=3,×5=15。取三者最小。
- 23最小是 12,填进 dp[10]。命中的指针 p2、p3 各前移一格(p2、p3 同时命中,一起前移——同时被两个指针命中的数只填一次)。
- 24最右 dp[10]=12 就是第 10 个丑数。整张表 1,2,3,4,5,6,8,9,10,12 正是从小到大的前 10 个丑数。
⚠️ 容易写错的地方
✗ 错:相等时只移一个指针
✓ 对:多个候选等于 min 时全部前移
否则会重复生成同一个数(如 6)
✗ 错:忘了 1 是丑数
✓ 对:dp[1]=1 作为起点
1 不含 2/3/5 之外的质因子
✗ 错:用 set+堆但忘判重
✓ 对:弹出时跳过重复值
×2×3 与 ×3×2 会撞同一个数
完整代码(Python / C++ / Java)
Python
def nthUglyNumber(n):
dp = [0] * (n + 1)
dp[1] = 1
p2 = p3 = p5 = 1
for i in range(2, n + 1):
c2, c3, c5 = dp[p2]*2, dp[p3]*3, dp[p5]*5
dp[i] = min(c2, c3, c5)
if dp[i] == c2: p2 += 1
if dp[i] == c3: p3 += 1
if dp[i] == c5: p5 += 1
return dp[n]C++
int nthUglyNumber(int n){
vector<int> dp(n + 1);
dp[1] = 1;
int p2 = 1, p3 = 1, p5 = 1;
for(int i = 2; i <= n; ++i){
int c2 = dp[p2]*2, c3 = dp[p3]*3, c5 = dp[p5]*5;
dp[i] = min({c2, c3, c5});
if(dp[i] == c2) ++p2;
if(dp[i] == c3) ++p3;
if(dp[i] == c5) ++p5;
}
return dp[n];
}Java
int nthUglyNumber(int n){
int[] dp = new int[n + 1];
dp[1] = 1;
int p2 = 1, p3 = 1, p5 = 1;
for(int i = 2; i <= n; i++){
int c2 = dp[p2]*2, c3 = dp[p3]*3, c5 = dp[p5]*5;
dp[i] = Math.min(c2, Math.min(c3, c5));
if(dp[i] == c2) p2++;
if(dp[i] == c3) p3++;
if(dp[i] == c5) p5++;
}
return dp[n];
}复杂度
时间
O(n)
生成 n 个丑数,每个 O(1)
空间
O(n)
dp 数组存前 n 个丑数
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 丑数 II 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么不能直接从 1 往上逐个判断是不是丑数?+
能做但慢。丑数越往后越稀疏,逐个整数去反复除 2、3、5 判断,会在海量非丑数上做无用试除,第 n 个丑数本身可能是个很大的数,中间要跳过的整数远多于 n。三指针 DP 反过来,只用已有丑数乘 2、3、5 直接按序造出下一个,每个新丑数摊下来 O(1),总共 O(n)。
相等时如果只移动一个指针会怎样?+
会生成重复的丑数。以 6 为例,它既是 dp[p2]×2 又是 dp[p3]×3,如果这一步只把 p2 前移、留着 p3,那么下一步 dp[p3]×3 还会再算出一个 6,dp 里就出现两个 6,后面所有丑数整体串位、答案偏小。正确做法是三个判断各自独立,凡候选等于当前最小值的指针都前移,一步可能同时移两个或三个。
能推广到「只含给定质因子集合」的丑数吗?+
能,这就是 LeetCode 313 超级丑数。把 2、3、5 换成给定的质因子数组,每个质因子配一个指针,每步取所有「指针指向的丑数 × 对应质因子」候选里的最小值,命中的指针全部前移,思路和三指针完全一样,只是指针数量从 3 变成质因子的个数。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 丑数 II 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。