web-dev-qa-db-ja.com

n番目の醜い数

素因数が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 * 3、ugly_number * 5はすべて醜いです。それでも遅いです。私はログのいくつかのプロパティを使用してみました-これは乗算から加算までこの問題を減らすので-しかし、まだそれほど運ではありません。これを皆さんと共有すると思いました。何か面白いアイデアはありますか?

「エラトステネスのふるい」に似たコンセプトを使用する(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番目の醜い数を見つけようとしています。

41
Anil Katti

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[]_に格納することは実際にはオプションではありません。

編集
仕事からのレクリエーション(愚かなHibernateのデバッグ)として、これは完全に線形のソリューションです。アイデアをありがとう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)

40
Nikita Rybak

私はn番目の醜い数を見つけることに取り組んでいます。これらの数値は、nが大きくなると、非常にまばらに分布することに注意してください。

私は与えられた数が醜いかどうかを計算する簡単なプログラムを書きました。

これは、あなたが解決しようとしている問題に対して間違ったアプローチのように見えます-それは少しshlemielアルゴリズムです。

素数を見つけるための Sieve of Eratosthenes アルゴリズムに精通していますか?同様の何か(すべての醜い数が別の醜い数の2、3、または5倍であるという知識を利用する)は、おそらくこれを解決するためによりうまく機能します。

Sieveと比較しても、「ブール値の配列を保持し、上に行くにつれて可能性を排除する」という意味ではありません。以前の結果に基づいてソリューションを生成する一般的な方法について、より詳しく言及します。 Sieveが数値を取得し、候補セットからそのすべての倍数を削除する場合、この問題の適切なアルゴリズムは、空のセットで開始し、次にadd各醜い数の正しい倍数をそれに。

11
Anon.

私の答えはNikita Rybakによって与えられた正しい答えです。したがって、最初のアプローチの考えから2番目のアプローチの考えへの移行を見ることができます。

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リストに個別に追加できることです。このように、各リストは常にソートされたままとなり、次に少ない候補は常にこれらのリストの1つ以上のheadにある必要があります。

上記の3つのリストの使用を排除すると、Nikita Rybak 'の回答で2番目の実装に到達します。これは、それらの候補(3つのリストに含まれる)を必要なときにのみ評価することで行われるため、それらを格納する必要はありません。

簡単に言えば

最初のアプローチでは、すべての新しい候補を単一のデータ構造に入れますが、あまりにも多くのものが不適切に混同されるため、それは悪いことです。この貧弱な戦略では、構造に対してクエリを実行するたびに、必然的にO(log(tree size))時間の複雑さが伴います。ただし、それらを別々のキューに入れることにより、各クエリはO(1))しかとらないことがわかります。これが全体的なパフォーマンスがO(n)に低下する理由です!!!これは、 3つのリストは、それ自体で既にソートされています。

8
chanp

私はあなたがこの問題を準線形時間、おそらくO(n ^ {2/3})で解決できると信じています。

アイデアを与えるために、問題を単純化して2と3の因数のみを許可する場合、少なくとも2の最小の2のべき乗を検索することから始まるO(n ^ {1/2})時間を達成できます。 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))]

同じ考えが3つの許可された要素で機能するはずですが、コードはより複雑になります。因数分解のパワーの合計はO(n ^ {1/3})に低下しますが、より正確にするには、より多くの候補O(n ^ {2/3})を考慮する必要があります。

6
jonderry

基本的に、検索はO(n)にすることができます。

醜い数字の部分的な履歴を保持していると考えてください。今、各ステップで次のものを見つける必要があります。これは、履歴の数に2、3、または5を掛けたものに等しいはずです。それらの最小値を選択し、それを履歴に追加して、リストから最小値を5倍して、最大。

次の番号の検索が簡単になるため、高速になります。
分(最大* 2、最小* 5、中央から1つ* 3)、
リスト内の最大数よりも大きい。それらが乏しい場合、リストには常に数が少ないため、3を掛ける必要がある数の検索は高速になります。

4
ruslik

Oのn番目の醜い数(n ^(2/3))を見つけるには、jonderryのアルゴリズムがうまく機能します。関係する数値はhugeであるため、数値が醜いかどうかを確認しようとするアルゴリズムにはチャンスがないことに注意してください。

O(n log n)の時間とO(n)の空間で優先キューを使用することにより、昇順でn個の最小の醜い数字すべてを簡単に見つけることができます。数値1.次にn回繰り返します。優先度キューから最小の数値xを削除します。 xが以前に削除されていない場合、xは次に大きい醜い数であり、2x、3x、5xを優先度キューに追加します。 (優先キューという用語を知らない人は、ヒープソートアルゴリズムのヒープのようなものです)。これがアルゴリズムの始まりです:

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

実行時間の証明:キューから醜い数をn回抽出します。最初はキューに1つの要素があり、醜い数を抽出した後、3つの要素を追加して、その数を2増やします。したがって、n個の醜い数が見つかった後は、キューに最大2n + 1個の要素があります。要素の抽出は対数時間で実行できます。醜い数字よりも多くの数字を抽出しますが、最大でn個の醜い数字に加えて2n-1個の他の数字(n-1ステップ後にふるいにあった可能性があるもの)を抽出します。したがって、合計時間は、対数時間での3nアイテムの削除= O(n log n)未満であり、合計スペースは最大で2n + 1要素= O(n)です。

2
gnasher729

これがMLの正しい解決策です。関数ugly()は、ハミング番号のストリーム(レイジーリスト)を返します。関数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())));

これは最初の1年のCS作業でした:-)

2
fredley

ここでは良い答えがたくさんありますが、それらを理解するのに苦労していました。具体的には、承認されたものを含め、これらの回答のいずれかが Dijkstraの元の論文の公理2をどのように維持しているか

公理2. xがシーケンス内にある場合、2 * x、3 * x、および5 * xも同様です。

ホワイトボード化の後、公理2はアルゴリズムの各反復で不変ではなくであることが明らかになりましたが、実際にはアルゴリズム自体の目標です。各反復で、公理2の条件を復元しようとします。lastが結果シーケンスSの最後の値である場合、公理2は単に次のように言い換えることができます。

xの一部のSでは、Sの次の値は2x3x、および5xの最小値で、lastより大きい値です。この公理を2 'としましょう。

したがって、xが見つかった場合、2x3x、および5xの最小値を一定の時間で計算し、それをSに追加できます。

しかし、どのようにxを見つけるのでしょうか。 1つのアプローチは、私たちがしません。代わりに、新しい要素eSに追加するたびに、2e3e、および5eを計算し、それらを最小優先度キューに追加します。この演算は、eSにあることを保証するため、PQの最上位要素を抽出するだけで公理2 'が満たされます。

このアプローチは機能しますが、問題は、使用しない可能性のある多数の数値を生成することです。例については this の回答を参照してください。ユーザーがS(5)の5番目の要素を必要とする場合、その時点のPQは6 6 8 9 10 10 12 15 15 20 25を保持します。このスペースを無駄にすることはできませんか?

結局のところ、私たちはより良いことができるのです。これらすべての数値を保存する代わりに、2i3j、および5kの各倍数ごとに3つのカウンターを維持するだけです。これらは、Sの次の数値の候補です。それらの1つを選択すると、対応するカウンターのみがインクリメントされ、他の2つはインクリメントされません。そうすることで、すべての倍数を熱心に生成するのではなく、最初のアプローチで空間問題を解決します。

n = 8の予行演習、つまり数値9を見てみましょう。ダイクストラの論文の公理1で述べられているように、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} |
+---------+---+---+---+----+----+----+-------------------+

最小候補6がすでに追加されているため、Sは反復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)))
}
2
Abhijit Sarkar

動的プログラミング(DP)を使用してn番目の醜い数を計算できると思います)。完全な説明は 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;
}

かなり高速であることがわかります。[〜#〜] max [〜#〜]の値を変更して、より高い醜い数字

1

これが私のコードです。アイデアは、数値を2(それが余り0になるまで)で割り、次に3と5で割ることです。ついにその数が1になった場合、それは醜い数です。 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++;
    }

}
0