题目描述
思路解析动画文字版
丑数序列本身是有结构的:已有丑数乘 2、乘 3、乘 5 仍然是丑数。我们应该只在丑数候选里推进。
不变量:dp[0..i-1] 已经是递增且不重复的前 i 个丑数;p2、p3、p5 分别指向下一次乘 2、3、5 时还没被使用的位置。
初始化:第 1 个丑数是 1:1 被视为第一个丑数。三个指针都从 0 出发,表示下一批候选分别是 2、3、5。
填 #2:三候选 2、3、5,取 2:最小候选是 2,放进 dp[1]。因为 2 来自 p2 这条线,所以 p2 前进,避免下次还生成 2。
填 #3:候选 4、3、5,取 3:p2 已经指向 dp[1]=2,所以乘 2 候选变成 4。当前最小是 3,p3 前进。
填 #4:候选 4、6、5,取 4:4 是 2 乘 2 得到的丑数。填入后 p2 继续前进,下一次乘 2 会看 dp[2]=3。
填 #5:候选 6、6、5,取 5:5 只来自乘 5 这条线,所以只移动 p5。现在前五个丑数是 1、2、3、4、5。
填 #6:候选 6、6、10,取 6:6 很关键:它既可以由 3×2 得到,也可以由 2×3 得到。如果只移动一个指针,下一轮会再生成一次 6。
6 同时命中 2 和 3,两指针都前进:去重靠的就是“所有命中的指针都动”。代码必须写三个独立 if,不能写成 if/elif/elif。
填 #7:候选 8、9、10,取 8:指针移动后,下一批候选自然跳过了重复的 6,继续得到 8。
填 #8:候选 10、9、10,取 9:9 来自 3×3,填入后 p3 前进。dp 始终保持递增。
填 #9:10 同时来自 2 和 5:10 也是重复候选:5×2 和 2×5 都能得到它,所以 p2、p5 必须一起移动。
填 #10:候选 12、12、15,取 12:第 10 格填入 12,和题目示例一致。真实代码会一直填到第 n 格,最后返回 dp[-1]。
看到“由已有结果乘几个固定因子生成下一个最小值”,可以联想到多路合并和指针去重。
参考代码
class Solution: def nthUglyNumber(self, n): dp = [1] * n p2 = p3 = p5 = 0 for i in range(1, n): a = dp[p2] * 2 b = dp[p3] * 3 c = dp[p5] * 5 nxt = min(a, b, c) dp[i] = nxt if a == nxt: p2 += 1 if b == nxt: p3 += 1 if c == nxt: p5 += 1 return dp[-1]复杂度
- 时间复杂度:O(n),每一轮生成一个新的丑数,总共生成 n 个。
- 空间复杂度:O(n),dp 数组保存前 n 个丑数。
易错点
这道题到这就讲完了。动画和文字是同一套思路——别光看,关掉页面自己默写一遍。然后顺着主线继续:
最长递增子序列
LeetCode 300 · 中等 · 沿着 动态规划套路 继续往下推进
把这道题真正学会,再走
图解算法年卡 ¥99 /年
- ✓本题每步动画的吴师兄语音讲解(全站陆续覆盖)
- ✓小欧带 8 步通关训练——追问到不看答案也能写对、能 30 秒讲给面试官听
- ✓学习报告,记录每道题的掌握程度
76k+ GitHub Star · 吴师兄开源图解算法,几十万开发者在看的算法讲解
想成体系刷透这类套路?去图解算法专题