UVA136 丑数 Ugly Numbers

这道题是经典的“丑数”生成问题。由于需要按顺序找到第 \(1500\) 个丑数,且丑数是由 \(2, 3, 5\) 的乘积构成的,我们可以利用 小根堆(最小优先队列) 来解决。

解题思路

  1. 核心思想
    • 丑数序列是递增的。
    • 如果我们已经有一个丑数 \(x\),那么 \(2x, 3x, 5x\) 也是丑数。
    • 我们可以从最小的丑数 \(1\) 开始,每次取出当前最小的丑数,生成新的丑数并放入池中,直到取到第 \(1500\) 个。
  2. 数据结构
    • 使用 std::priority_queue(配合 greater 实现小根堆)来维护当前的丑数集合,确保每次都能取出最小值。
    • 使用 std::set 或者是判断逻辑来去重。因为同一个数可能被多次生成(例如 \(6\) 可以由 \(2 \times 3\) 生成,也可以由 \(3 \times 2\) 生成)。
  3. 注意事项
    • 数据可能会溢出 32 位整数,计算过程中建议使用 long long
    • 题目没有输入,程序直接运行并输出结果即可。

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 去重。

解题思路

  1. 核心逻辑

    我们维护一个数组 ugly 来按顺序存储丑数。

    下一个丑数一定是由之前某个已知的丑数乘以 \(2\)\(3\)\(5\) 得到的。

  2. 三指针机制

    • 定义三个指针 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\)
    • 取最小值作为下一个丑数。
  3. 移动指针与去重

    • 选中了哪个候选值,就将对应的指针向后移一步(\(+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\),两种方法都能瞬间完成,但三指针法是面试或竞赛中这类问题的标准“最优解”。

posted @ 2026-01-27 21:18  张一信奥  阅读(1)  评论(0)    收藏  举报