UVA136 丑数 Ugly Numbers
这道题是经典的“丑数”生成问题。由于需要按顺序找到第 \(1500\) 个丑数,且丑数是由 \(2, 3, 5\) 的乘积构成的,我们可以利用 小根堆(最小优先队列) 来解决。
解题思路
- 核心思想:
- 丑数序列是递增的。
- 如果我们已经有一个丑数 \(x\),那么 \(2x, 3x, 5x\) 也是丑数。
- 我们可以从最小的丑数 \(1\) 开始,每次取出当前最小的丑数,生成新的丑数并放入池中,直到取到第 \(1500\) 个。
- 数据结构:
- 使用
std::priority_queue(配合greater实现小根堆)来维护当前的丑数集合,确保每次都能取出最小值。 - 使用
std::set或者是判断逻辑来去重。因为同一个数可能被多次生成(例如 \(6\) 可以由 \(2 \times 3\) 生成,也可以由 \(3 \times 2\) 生成)。
- 使用
- 注意事项:
- 数据可能会溢出 32 位整数,计算过程中建议使用
long long。 - 题目没有输入,程序直接运行并输出结果即可。
- 数据可能会溢出 32 位整数,计算过程中建议使用
C++ 代码实现
这是使用优先队列的简洁写法:
#include <iostream>
#include <vector>
#include <queue>
#include <set>
using namespace std;
int main() {
// 定义小根堆,类型为 long long 防止溢出
priority_queue<long long, vector<long long>, greater<long long>> pq;
// 使用 set 进行去重
set<long long> seen;
// 初始化:放入第一个丑数 1
pq.push(1);
seen.insert(1);
long long current_ugly = 0;
// 循环提取 1500 次最小值
for (int i = 0; i < 1500; ++i) {
current_ugly = pq.top(); // 取出堆顶(当前最小值)
pq.pop();
// 将当前丑数分别乘以 2, 3, 5
long long next_vals[3] = {current_ugly * 2, current_ugly * 3, current_ugly * 5};
for (long long val : next_vals) {
// 如果该数未出现过,则入堆并记录
if (seen.find(val) == seen.end()) {
seen.insert(val);
pq.push(val);
}
}
}
// 输出格式必须严格匹配题目要求
cout << "The 1500'th ugly number is " << current_ugly << "." << endl;
return 0;
}
关键点总结
- 去重:必须处理重复生成的数字(如 \(6, 10, 15\) 等),否则计数会出错。使用
std::set是最简单的去重方法。 - 溢出:虽然第 \(1500\) 个丑数(\(859,963,392\))在
int范围内,但在生成后续候选数(如乘以 \(5\))的过程中可能会短暂超出int范围,因此全程使用long long更安全。 - 输出格式:注意题目要求的标点符号,单词
1500'th中的单引号不能漏掉。
如果你需要极致的运行效率,也可以使用 三指针法(动态规划),时间复杂度为 \(O(N)\),但优先队列法在 \(N=1500\) 时已经足够快且更易于理解。
这是 三指针法(动态规划) 的解法。相比优先队列,\(O(N)\) 的解法效率更高,因为它避免了堆操作的 \(O(\log N)\) 开销,也不需要 set 去重。
解题思路
-
核心逻辑:
我们维护一个数组
ugly来按顺序存储丑数。下一个丑数一定是由之前某个已知的丑数乘以 \(2\)、\(3\) 或 \(5\) 得到的。
-
三指针机制:
- 定义三个指针
p2,p3,p5,分别指向 “还没乘过 2”、“还没乘过 3”、“还没乘过 5” 的最小丑数位置。初始时都指向第一个丑数(下标 0)。 - 每次迭代,计算三个候选值:
- \(v_2 = \text{ugly}[p2] \times 2\)
- \(v_3 = \text{ugly}[p3] \times 3\)
- \(v_5 = \text{ugly}[p5] \times 5\)
- 取最小值作为下一个丑数。
- 定义三个指针
-
移动指针与去重:
- 选中了哪个候选值,就将对应的指针向后移一步(\(+1\))。
- 关键点:如果多个候选值相等(例如 \(2 \times 3 = 6\) 和 \(3 \times 2 = 6\)),则对应的指针都要移动。这一步巧妙地解决了去重问题。
C++ 代码实现
#include <iostream>
#include <vector>
#include <algorithm> // 用于 std::min
using namespace std;
int main() {
// 存储丑数的数组,预分配空间
vector<long long> ugly(1500);
ugly[0] = 1; // 第一个丑数是 1
// 定义三个指针,初始化为 0
int p2 = 0, p3 = 0, p5 = 0;
// 从第 2 个丑数开始计算 (下标 1 到 1499)
for (int i = 1; i < 1500; ++i) {
// 计算三个候选值
long long next_val = min({ugly[p2] * 2, ugly[p3] * 3, ugly[p5] * 5});
ugly[i] = next_val;
// 更新指针(注意这里不能用 else if,必须处理相等的情况以去重)
if (next_val == ugly[p2] * 2) p2++;
if (next_val == ugly[p3] * 3) p3++;
if (next_val == ugly[p5] * 5) p5++;
}
// 输出结果
cout << "The 1500'th ugly number is " << ugly[1499] << "." << endl;
return 0;
}
为什么这个方法更好?
| 特性 | 优先队列法 (Heap) | 三指针法 (DP) |
|---|---|---|
| 时间复杂度 | \(O(N \log N)\) | \(O(N)\) |
| 空间复杂度 | \(O(N)\) (堆 + Set) | \(O(N)\) (仅一个数组) |
| 去重逻辑 | 需要 set 辅助 |
通过指针同步移动自然去重 |
| 运行速度 | 较快 | 极快 |
对于 \(N=1500\),两种方法都能瞬间完成,但三指针法是面试或竞赛中这类问题的标准“最优解”。

浙公网安备 33010602011771号