45

只有质因数为 2、3 或 5 的数称为丑数

例子:

1, 2, 3, 4, 5, 6, 8, 9, 10, 12, 15, ... 

1 可以认为是 2^0。

我正在努力寻找第 n丑数。n请注意,随着变大,这些数字的分布非常稀疏。

我写了一个简单的程序来计算给定的数字是否丑陋。因为n > 500- 它变得超级慢。我尝试使用记忆 - 观察:ugly_number * 2,,都是丑陋的ugly_number * 3ugly_number * 5即使这样它也很慢。我尝试使用 log 的一些属性——因为这将把这个问题从乘法减少到加法——但是,还没有多少运气。想把这个分享给大家。有什么有趣的想法吗?

使用类似于埃拉托色尼筛法的概念(感谢 Anon)

    for (int i(2), uglyCount(0); ; i++) {
        if (i % 2 == 0)
            continue;
        if (i % 3 == 0)
            continue;
        if (i % 5 == 0)
            continue;
        uglyCount++;
        if (uglyCount == n - 1)
            break;
    }

i是第 n丑数。

即使这样也很慢。我试图找到第 1500丑陋的数字。

4

13 回答 13

41

Java中一个简单的快速解决方案。使用Anon 描述的方法。.
TreeSet只是一个能够返回其中最小元素的容器。(不存储重复项。)

    int n = 20;
    SortedSet<Long> next = new TreeSet<Long>();
    next.add((long) 1);

    long cur = 0;
    for (int i = 0; i < n; ++i) {
        cur = next.first();
        System.out.println("number " + (i + 1) + ":   " + cur);

        next.add(cur * 2);
        next.add(cur * 3);
        next.add(cur * 5);
        next.remove(cur);
    }

由于第 1000 个丑数是 51200000,因此将它们存储在其中bool[]并不是一个真正的选择。

编辑
作为工作的娱乐(调试愚蠢的休眠),这里是完全线性的解决方案。感谢marcog的想法!

    int n = 1000;

    int last2 = 0;
    int last3 = 0;
    int last5 = 0;

    long[] result = new long[n];
    result[0] = 1;
    for (int i = 1; i < n; ++i) {
        long prev = result[i - 1];

        while (result[last2] * 2 <= prev) {
            ++last2;
        }
        while (result[last3] * 3 <= prev) {
            ++last3;
        }
        while (result[last5] * 5 <= prev) {
            ++last5;
        }

        long candidate1 = result[last2] * 2;
        long candidate2 = result[last3] * 3;
        long candidate3 = result[last5] * 5;

        result[i] = Math.min(candidate1, Math.min(candidate2, candidate3));
    }

    System.out.println(result[n - 1]);

这个想法是计算a[i],我们可以使用a[j]*2一些j < i。但我们还需要确保 1)a[j]*2 > a[i - 1]和 2)j尽可能小。
那么,a[i] = min(a[j]*2, a[k]*3, a[t]*5)

于 2011-01-05T01:26:26.983 回答
12

我正在努力寻找第 n 个丑数。请注意,随着 n 变大,这些数字的分布非常稀疏。

我写了一个简单的程序来计算给定的数字是否丑陋。

对于您要解决的问题,这看起来像是错误的方法——它有点像 shlemiel 算法。

您熟悉用于寻找素数的埃拉托色尼筛算法吗?类似的东西(利用每个丑数是另一个丑数的 2、3 或 5 倍的知识)可能会更好地解决这个问题。

与 Sieve 相比,我并不是说“保持一系列布尔值并在上升时消除可能性”。我更多地指的是根据以前的结果生成解决方案的一般方法。筛子得到一个数字,然后从候选集中删除它的所有倍数,解决这个问题的一个好的算法将从一个空集开始,然后每个丑数的正确倍数添加到其中。

于 2011-01-05T01:23:56.763 回答
9

我的答案是指Nikita Rybak给出的正确答案。这样人们就可以看到从第一种方法的想法到第二种方法的转变。

from collections import deque
def hamming():
    h=1;next2,next3,next5=deque([]),deque([]),deque([])
    while True:
        yield h
        next2.append(2*h)
        next3.append(3*h)
        next5.append(5*h)
        h=min(next2[0],next3[0],next5[0])
        if h == next2[0]: next2.popleft()
        if h == next3[0]: next3.popleft()
        if h == next5[0]: next5.popleft()

Nikita Rybak 的第一种方法的不同之处在于,不是将下一个候选者添加到单个数据结构(即树集)中,而是可以将它们中的每一个分别添加到 3 个 FIFO 列表中。这样,每个列表将一直保持排序,并且下一个最小的候选人必须始终位于一个或多个这些列表的头部。

如果我们消除上面三个列表的使用,我们会在Nikita Rybak的回答中得到第二个实现。这是通过仅在需要时评估那些候选者(将包含在三个列表中)来完成的,因此不需要存储它们。

简单地说

在第一种方法中,我们将每个新的候选对象放入单个数据结构中,这很糟糕,因为太多东西被不明智地混在一起了。每次我们对结构进行查询时,这种糟糕的策略不可避免地需要 O(log(tree size)) 时间复杂度。但是,通过将它们放入单独的队列中,您会看到每个查询只需要 O(1),这就是为什么整体性能会降低到 O(n)!!!这是因为这三个列表中的每一个都已经单独排序。

于 2012-01-25T17:18:08.367 回答
6

我相信你可以在亚线性时间内解决这个问题,可能是 O(n^{2/3})。

给你一个想法,如果你将问题简化为只允许 2 和 3 的因子,你可以通过搜索至少与第 n 个丑数,然后生成 O(n^{1/2}) 个候选者列表。这段代码应该让你知道如何去做。它依赖于这样一个事实,即仅包含 2 和 3 的幂的第 n 个数具有指数之和为 O(n^{1/2}) 的素数分解。

def foo(n):
  p2 = 1  # current power of 2
  p3 = 1  # current power of 3
  e3 = 0  # exponent of current power of 3
  t = 1   # number less than or equal to the current power of 2
  while t < n:
    p2 *= 2
    if p3 * 3 < p2:
      p3 *= 3
      e3 += 1
    t += 1 + e3
  candidates = [p2]
  c = p2
  for i in range(e3):
    c /= 2
    c *= 3
    if c > p2: c /= 2
    candidates.append(c)
  return sorted(candidates)[n - (t - len(candidates))]

同样的想法应该适用于三个允许的因素,但代码变得更加复杂。分解的幂的总和下降到 O(n^{1/3}),但您需要考虑更多的候选者,O(n^{2/3}) 更精确。

于 2011-01-05T06:40:32.127 回答
6

这里有很多好的答案,但我无法理解这些答案,特别是这些答案中的任何一个,包括已接受的答案,如何维护Dijkstra 原始论文中的公理 2 :

公理 2。如果 x 在序列中,则 2 * x、3 * x 和 5 * x 也是。

在一些白板之后,很明显公理 2在算法的每次迭代中都不是不变量,而是算法本身的目标。在每次迭代中,我们尝试恢复公理 2 中的条件。如果last是结果序列中的最后一个值,则S公理 2 可以简单地改写为:

对于某些xin S, in 的下一个值是、 和S的最小值2x, 即大于。我们称此公理 2'。3x5xlast

因此,如果我们能找到,我们可以在恒定时间内计算、 和x的最小值2x,并将其添加到。3x5xS

但是我们怎么找到x呢?一种方法是,我们不这样做;相反,每当我们向 中添加新元素eS,我们都会计算2e3e5e,并将它们添加到最小优先级队列中。由于此操作保证e在 中S,简单地提取 PQ 的顶部元素满足公理 2'。

这种方法有效,但问题是我们生成了一堆我们最终可能不会使用的数字。有关示例,请参见此答案;如果用户想要S(5) 中的第 5 个元素,那么此时的 PQ 成立6 6 8 9 10 10 12 15 15 20 25。我们不能浪费这个空间吗?

事实证明,我们可以做得更好。我们不存储所有这些数字,而是简单地为每个倍数维护三个计数器,即2i3j5k。这些是 中下一个数字的候选者S。当我们选择其中一个时,我们只增加相应的计数器,而不增加其他两个。通过这样做,我们不会急切地生成所有的倍数,从而用第一种方法解决空间问题。

让我们看一下 的试运行n = 8,即数字91正如 Dijkstra 的论文中公理 1 所述,我们从 开始。

+---------+---+---+---+----+----+----+-------------------+
| #       | i | j | k | 2i | 3j | 5k | S                 |
+---------+---+---+---+----+----+----+-------------------+
| initial | 1 | 1 | 1 | 2  | 3  | 5  | {1}               |
+---------+---+---+---+----+----+----+-------------------+
| 1       | 1 | 1 | 1 | 2  | 3  | 5  | {1,2}             |
+---------+---+---+---+----+----+----+-------------------+
| 2       | 2 | 1 | 1 | 4  | 3  | 5  | {1,2,3}           |
+---------+---+---+---+----+----+----+-------------------+
| 3       | 2 | 2 | 1 | 4  | 6  | 5  | {1,2,3,4}         |
+---------+---+---+---+----+----+----+-------------------+
| 4       | 3 | 2 | 1 | 6  | 6  | 5  | {1,2,3,4,5}       |
+---------+---+---+---+----+----+----+-------------------+
| 5       | 3 | 2 | 2 | 6  | 6  | 10 | {1,2,3,4,5,6}     |
+---------+---+---+---+----+----+----+-------------------+
| 6       | 4 | 2 | 2 | 8  | 6  | 10 | {1,2,3,4,5,6}     |
+---------+---+---+---+----+----+----+-------------------+
| 7       | 4 | 3 | 2 | 8  | 9  | 10 | {1,2,3,4,5,6,8}   |
+---------+---+---+---+----+----+----+-------------------+
| 8       | 5 | 3 | 2 | 10 | 9  | 10 | {1,2,3,4,5,6,8,9} |
+---------+---+---+---+----+----+----+-------------------+

请注意,S在第 6 次迭代中它没有增长,因为6之前已经添加了最小候选者。为了避免必须记住所有先前元素的问题,我们修改我们的算法以在对应的倍数等于最小候选值时递增所有计数器。这将我们带到以下 Scala 实现。

def hamming(n: Int): Seq[BigInt] = {
  @tailrec
  def next(x: Int, factor: Int, xs: IndexedSeq[BigInt]): Int = {
    val leq = factor * xs(x) <= xs.last
    if (leq) next(x + 1, factor, xs)
    else x
  }

  @tailrec
  def loop(i: Int, j: Int, k: Int, xs: IndexedSeq[BigInt]): IndexedSeq[BigInt] = {
    if (xs.size < n) {
      val a = next(i, 2, xs)
      val b = next(j, 3, xs)
      val c = next(k, 5, xs)
      val m = Seq(2 * xs(a), 3 * xs(b), 5 * xs(c)).min

      val x = a + (if (2 * xs(a) == m) 1 else 0)
      val y = b + (if (3 * xs(b) == m) 1 else 0)
      val z = c + (if (5 * xs(c) == m) 1 else 0)

      loop(x, y, z, xs :+ m)
    } else xs
  }

  loop(0, 0, 0, IndexedSeq(BigInt(1)))
}
于 2019-02-11T03:49:39.223 回答
4

基本上可以进行 O(n) 的搜索:

考虑到您保留了丑陋数字的部分历史记录。现在,在每一步你都必须找到下一个。它应该等于历史记录中的一个数字乘以 2、3 或 5。选择其中最小的一个,将其添加到历史记录中,然后从中删除一些数字,以便列表中最小的乘以 5 大于最大的。

它会很快,因为搜索下一个数字会很简单:
min(最大 * 2, 最小 * 5, 中间一个 * 3),
即大于列表中的最大数字。如果它们很稀少,则列表将始终包含很少的数字,因此搜索必须乘以 3 的数字会很快。

于 2011-01-05T01:26:23.507 回答
2

这是 ML 中的正确解决方案。函数丑陋()将返回汉明数流(惰性列表)。函数 nth 可以在这个流上使用。

这使用了 Sieve 方法,仅在需要时计算下一个元素。

datatype stream = Item of int * (unit->stream);
fun cons (x,xs) = Item(x, xs);
fun head (Item(i,xf)) = i;
fun tail (Item(i,xf)) = xf();
fun maps f xs = cons(f (head xs), fn()=> maps f (tail xs));

fun nth(s,1)=head(s)
  | nth(s,n)=nth(tail(s),n-1);

fun merge(xs,ys)=if (head xs=head ys) then
                   cons(head xs,fn()=>merge(tail xs,tail ys))
                 else if (head xs<head ys) then
                   cons(head xs,fn()=>merge(tail xs,ys))
                 else
                   cons(head ys,fn()=>merge(xs,tail ys));

fun double n=n*2;
fun triple n=n*3;

fun ij()=
    cons(1,fn()=>
      merge(maps double (ij()),maps triple (ij())));

fun quint n=n*5;

fun ugly()=
    cons(1,fn()=>
      merge((tail (ij())),maps quint (ugly())));

这是第一年的 CS 工作:-)

于 2011-01-05T01:29:08.777 回答
2

To find the n-th ugly number in O (n^(2/3)), jonderry's algorithm will work just fine. Note that the numbers involved are huge so any algorithm trying to check whether a number is ugly or not has no chance.

Finding all of the n smallest ugly numbers in ascending order is done easily by using a priority queue in O (n log n) time and O (n) space: Create a priority queue of numbers with the smallest numbers first, initially including just the number 1. Then repeat n times: Remove the smallest number x from the priority queue. If x hasn't been removed before, then x is the next larger ugly number, and we add 2x, 3x and 5x to the priority queue. (If anyone doesn't know the term priority queue, it's like the heap in the heapsort algorithm). Here's the start of the algorithm:

1 -> 2 3 5
1 2 -> 3 4 5 6 10
1 2 3 -> 4 5 6 6 9 10 15
1 2 3 4 -> 5 6 6 8 9 10 12 15 20
1 2 3 4 5 -> 6 6 8 9 10 10 12 15 15 20 25
1 2 3 4 5 6 -> 6 8 9 10 10 12 12 15 15 18 20 25 30
1 2 3 4 5 6 -> 8 9 10 10 12 12 15 15 18 20 25 30
1 2 3 4 5 6 8 -> 9 10 10 12 12 15 15 16 18 20 24 25 30 40

Proof of execution time: We extract an ugly number from the queue n times. We initially have one element in the queue, and after extracting an ugly number we add three elements, increasing the number by 2. So after n ugly numbers are found we have at most 2n + 1 elements in the queue. Extracting an element can be done in logarithmic time. We extract more numbers than just the ugly numbers but at most n ugly numbers plus 2n - 1 other numbers (those that could have been in the sieve after n-1 steps). So the total time is less than 3n item removals in logarithmic time = O (n log n), and the total space is at most 2n + 1 elements = O (n).

于 2015-08-31T15:48:35.207 回答
1

I guess we can use Dynamic Programming (DP) and compute nth Ugly Number. Complete explanation can be found at http://www.geeksforgeeks.org/ugly-numbers/

#include <iostream>
#define MAX 1000

using namespace std;

// Find Minimum among three numbers
long int min(long int x, long int y, long int z) {

    if(x<=y) {
        if(x<=z) {
            return x;
        } else {
            return z;
        }
    } else {
        if(y<=z) {
            return y;
        } else {
            return z;
        }
    }   
}


// Actual Method that computes all Ugly Numbers till the required range
long int uglyNumber(int count) {

    long int arr[MAX], val;

    // index of last multiple of 2 --> i2
    // index of last multiple of 3 --> i3
    // index of last multiple of 5 --> i5
    int i2, i3, i5, lastIndex;

    arr[0] = 1;
    i2 = i3 = i5 = 0;
    lastIndex = 1;


    while(lastIndex<=count-1) {

        val = min(2*arr[i2], 3*arr[i3], 5*arr[i5]);

        arr[lastIndex] = val;
        lastIndex++;

        if(val == 2*arr[i2]) {
            i2++;
        }
        if(val == 3*arr[i3]) {
            i3++;
        }
        if(val == 5*arr[i5]) {
            i5++;
        }       
    }

    return arr[lastIndex-1];

}

// Starting point of program
int main() {

    long int num;
    int count;

    cout<<"Which Ugly Number : ";
    cin>>count;

    num = uglyNumber(count);

    cout<<endl<<num;    

    return 0;
}

We can see that its quite fast, just change the value of MAX to compute higher Ugly Number

于 2014-09-09T22:50:00.580 回答
1

并行使用 3 个生成器并在每次迭代中选择最小的,这是一个 C 程序,可以在不到 1 秒的时间内计算出所有低于2128的丑数:

#include <limits.h>
#include <stdio.h>

#if 0
typedef unsigned long long ugly_t;
#define UGLY_MAX  (~(ugly_t)0)
#else
typedef __uint128_t ugly_t;
#define UGLY_MAX  (~(ugly_t)0)
#endif

int print_ugly(int i, ugly_t u) {
    char buf[64], *p = buf + sizeof(buf);

    *--p = '\0';
    do { *--p = '0' + u % 10; } while ((u /= 10) != 0);
    return printf("%d: %s\n", i, p);
}

int main() {
    int i = 0, n2 = 0, n3 = 0, n5 = 0;
    ugly_t u, ug2 = 1, ug3 = 1, ug5 = 1;
#define UGLY_COUNT  110000
    ugly_t ugly[UGLY_COUNT];

    while (i < UGLY_COUNT) {
        u = ug2;
        if (u > ug3) u = ug3;
        if (u > ug5) u = ug5;
        if (u == UGLY_MAX)
            break;
        ugly[i++] = u;
        print_ugly(i, u);
        if (u == ug2) {
            if (ugly[n2] <= UGLY_MAX / 2)
                ug2 = 2 * ugly[n2++];
            else
                ug2 = UGLY_MAX;
        }
        if (u == ug3) {
            if (ugly[n3] <= UGLY_MAX / 3)
                ug3 = 3 * ugly[n3++];
            else
                ug3 = UGLY_MAX;
        }
        if (u == ug5) {
            if (ugly[n5] <= UGLY_MAX / 5)
                ug5 = 5 * ugly[n5++];
            else
                ug5 = UGLY_MAX;
        }
    }
    return 0;
}

以下是最后 10 行输出:

100517: 338915443777200000000000000000000000000
100518: 339129266201729628114355465608000000000
100519:339186548067800934969350553600000000000
100520: 339298130282929870605468750000000000000
100521:339467078447341918945312500000000000000
100522: 339569540691046437734055936000000000000
100523: 339738624000000000000000000000000000000
100524:339952965770562084651663360000000000000
100525:340010386766614455386112000000000000000
100526: 340122240000000000000000000000000000000

这是可用于 QuickJS 的 Javascript 版本:

import * as std from "std";

function main() {
    var i = 0, n2 = 0, n3 = 0, n5 = 0;
    var u, ug2 = 1n, ug3 = 1n, ug5 = 1n;
    var ugly = [];

    for (;;) {
        u = ug2;
        if (u > ug3) u = ug3;
        if (u > ug5) u = ug5;
        ugly[i++] = u;
        std.printf("%d: %s\n", i, String(u));
        if (u >= 0x100000000000000000000000000000000n)
            break;
        if (u == ug2)
            ug2 = 2n * ugly[n2++];
        if (u == ug3)
            ug3 = 3n * ugly[n3++];
        if (u == ug5)
            ug5 = 5n * ugly[n5++];
    }
    return 0;
}
main();
于 2020-04-10T16:49:44.873 回答
0

这是我的代码,想法是将数字除以 2(直到余数为 0),然后是 3 和 5。如果最后这个数字变成了一个,那就是一个丑陋的数字。您可以计算甚至打印所有丑陋的数字,直到 n。

int count = 0;
for (int i = 2; i <= n; i++) {
    int temp = i;
    while (temp % 2 == 0) temp=temp / 2;
    while (temp % 3 == 0) temp=temp / 3;
    while (temp % 5 == 0) temp=temp / 5;
    if (temp == 1) {
        cout << i << endl;
        count++;
    }

}
于 2017-08-11T16:49:05.613 回答
-3

这个问题可以在 O(1) 中完成。

如果我们删除 1 并查看 2 到 30 之间的数字,我们会注意到有 22 个数字。

现在,对于上面 22 个数字中的任何数字 x,在 31 和 60 之间都会有一个数字 x + 30,这也是丑陋的。因此,我们可以找到至少 22 个介于 31 和 60 之间的数字。现在对于 31 和 60 之间的每个丑数,我们可以将其写为 s + 30。所以 s 也将是丑数,因为 s + 30 可以被 2、3 整除,或 5。因此,在 31 和 60 之间将恰好有 22 个数字。此后,可以对每个 30 个数字块重复此逻辑。

因此,前 30 个号码中将有 23 个号码,之后每 30 个号码将有 22 个号码。也就是说,前 23 个 uglies 将出现在 1 和 30 之间,45 个 uglies 将出现在 1 和 60 之间,67 个 uglies 将出现在 1 和 30 之间等等。

现在,如果给定 n,比如 137,我可以看到 137/22 = 6.22。答案将在 6*30 和 7*30 之间或 180 和 210 之间。到 180 时,我将在 180 处获得 6*22 + 1 = 第 133 个丑数。我将在 210 处获得第 154 个丑数。所以我正在寻找第 4 个丑数(因为 137 = 133 + 4)在区间 [2, 30] 中,即 5。第 137 个丑数是 180 + 5 = 185。

另一个例子:如果我想要第 1500 个丑数,我数 1500/22 = 68 个块。因此,我将在 30*68 = 2040 处得到 22*68 + 1 = 1497th 2044.

我可以简单地在 [2, 30] 之间构建一个丑数列表,并通过在 O(1) 中查找来简单地找到答案。

于 2014-10-02T17:45:21.660 回答
-3

这是基于合并三个排序列表的想法的另一种O(n)方法(Python 解决方案)。挑战在于以递增的顺序找到下一个丑数。例如,我们知道前七个丑数是[1,2,3,4,5,6,8]。丑陋的数字实际上来自以下三个列表:

  • 清单 1:1*2、2*2、3*2、4*2、5*2、6*2、8*2 ...(每个丑数乘以 2)
  • 清单 2:1*3、2*3、3*3、4*3、5*3、6*3、8*3 ...(将每个丑数乘以 3)
  • 清单 3:1*5、2*5、3*5、4*5、5*5、6*5、8*5 ...(每个丑数乘以 5)

所以第n个丑数是上面三个列表合并的列表的第n个数字:

1、1*2、1*3、2*2、1*5、2*3 ...

def nthuglynumber(n):
    p2, p3, p5 = 0,0,0
    uglynumber = [1]
    while len(uglynumber) < n:
        ugly2, ugly3, ugly5 = uglynumber[p2]*2, uglynumber[p3]*3, uglynumber[p5]*5
        next = min(ugly2, ugly3, ugly5)
        if next == ugly2: p2 += 1        # multiply each number
        if next == ugly3: p3 += 1        # only once by each
        if next == ugly5: p5 += 1        # of the three factors
        uglynumber += [next]
    return uglynumber[-1]
  1. 第一步:从三个列表中计算三个下一个可能的丑数
    • ugly2, ugly3, ugly5 = uglynumber[p2]*2, uglynumber[p3]*3, uglynumber[p5]*5
  2. 第二步,找到下一个丑数作为上述三个中最小的一个:
    • next = min(ugly2, ugly3, ugly5)
  3. 第三步:如果丑数是下一个丑数,则向前移动指针
    • if next == ugly2: p2+=1
    • if next == ugly3: p3+=1
    • if next == ugly5: p5+=1
    • 注意:使用ifwithelif也不else
  4. 第四步:将下一个丑数添加到合并列表中uglynumber
    • uglynumber += [next]
于 2015-08-30T18:21:41.830 回答