2 ^ i * 5 ^ jから次に小さいものを印刷します。ここで、i、j> = 0


10

最近、電話によるテクニカルスクリーニングでこの質問を受けましたが、うまくいきませんでした。質問は以下に逐語的に含まれています。

{2^i * 5^j | i,j >= 0}ソートされたコレクションを生成します。次の最小値を継続的に印刷します。

例: { 1, 2, 4, 5, 8, 10...}

「次の最小」は、最小ヒープが関係していると思いますが、そこからどこに行くべきか本当にわかりませんでしたし、インタビュアーからの援助もありませんでした。

誰かがそのような問題を解決する方法についてアドバイスを持っていますか?


面接では、いつも思い出していただきたいと思います。O(n)メモリを使用すると、これは非常に簡単になります。または、入力nのエンコードサイズがlognになるため、少なくともO(logn)メモリを使用します。メモリソリューションのO(n)は指数メモリソリューションです。
InformedA 2014

回答:


14

問題を言い換えてみましょう。1から無限大までのすべての数値を出力し、数値に2と5以外の要素がないようにします。

以下は簡単なC#スニペットです。

for (int i = 1;;++i)
{
    int num = i;
    while(num%2 == 0) num/=2;
    while(num%5 == 0) num/=5;
    if(num == 1) Console.WriteLine(i);
}

キリアンの / QuestionCのアプローチは、はるかに高性能です。このアプローチのC#スニペット:

var itms = new SortedSet<int>();
itms.Add(1);
while(true)
{
    int cur = itms.Min;
    itms.Remove(itms.Min);
    itms.Add(cur*2);
    itms.Add(cur*5);
    Console.WriteLine(cur);
}

SortedSet 重複した挿入を防ぎます。

基本的に、シーケンスの次の番号がにあることを確認することで機能しitmsます。

このアプローチが有効であることの証明:
記述されたアルゴリズムは、フォーム内の任意の数値の出力後2^i*5^jに、セットに2^(i+1)*5^jとが含まれることを保証します2^i*5^(j+1)。シーケンスの次の数がであるとし2^p*5^qます。2^(p-1)*5^(q)またはの以前に出力された番号が存在している必要があります2^p*5^(q-1)(pもqも0と等しくない場合は両方)。そうでない場合は、2^p*5^q以降、次の番号ではない2^(p-1)*5^(q)2^p*5^(q-1)の両方小さくなっています。

2番目のスニペットはO(n)メモリを使用し(nは出力された数値の数です)、これはO(i+j) = O(n)(iとjがどちらもnより小さいため)、n個の数値をO(n log n)時間内に検出するためです。最初のスニペットは、指数時間で数値を見つけます。


1
こんにちは、私が望んでいる面接で混乱した理由がわかります。実際、提供されている例は、質問で説明されているセットからの出力です。1 = 2^0*5^0, 2 = 2^1*5^0, 4 = 2^2*5^0, 5 = 2^0*5^1, 8 = 2^3*5^0, 10 = 2^1*5^1
Justin Skiles 14

これらが繰り返される.Remove()と、.Add()ガベージコレクタから悪い行動の引き金になるだろうか、それは物事を把握でしょうか?
スノーボディ2014

1
@スノーボディ:オペレーションの質問はアルゴリズムの質問であるため、あまり関係ありません。それを無視して、あなたの最初の懸念は非常に大きな整数を扱うことです、これはガベージコレクタのオーバーヘッドよりずっと早く問題になるからです。
ブライアン

8

これはよくある一般的なインタビューの質問で、答えを知るのに役立ちます。これが私の個人用ベビーシートの関連エントリです。

  • 3 a 5 b 7 c の形式の番号を順番に生成するには、1から始めて、3つすべての後続(3、5、7)を補助構造に入れ、それから最小の数をリストに追加します。

つまり、これを効率的に解決するには、追加のソート済みバッファーを使用する2ステップのアプローチが必要です。(より長い説明は、Gayle McDowellによるCracking the Coding Interviewにあります。


3

CPUを犠牲にして、一定のメモリで実行する答えは次のとおりです。これは、元の質問(つまり、インタビュー中の回答)のコンテキストでは適切な回答ではありません。しかし、面接が24時間であれば、それほど悪くはありません。;)

アイデアは、有効な答えであるnがある場合、シーケンスの次は、2の累乗のn倍、5の累乗で除算する、または5の累乗をn倍して、 2のべき乗。均等に分割することを条件とします。(...または除数は1にすることができます;)この場合、2または5を掛けるだけです)

たとえば、625から640に移動するには、5 ** 4/2 ** 7を掛けます。または、より一般的には2 ** m * 5 ** n、あるm、nの値を掛けます。ここで、1は正で、1は負またはゼロです。乗数は、数値を均等に割ります。

今、トリッキーな部分は乗数を見つけることです。ただし、a)除数は数値を均等に除算する必要があります。b)乗数は1より大きくなければなりません(数値は増加し続けます)。c)1より大きい最小の乗数を選択した場合(つまり、1 <f <他のすべてのf) )、それが次のステップになることが保証されています。その次のステップが最低のステップになります。

厄介なのは、m、nの値を見つけることです。あきらめるのは2または5の数が非常に多いため、log(n)の可能性のみがありますが、丸めを処理するためのずさんな方法として、-1から+1の係数を追加する必要がありました。したがって、各ステップでO(log(n))を繰り返すだけで済みます。つまり、全体としてO(n log(n))です。

良いニュースは、値を取得して次の値を見つけるため、シーケンスのどこからでも開始できるということです。したがって、10億の次の1が必要な場合は、2/5または5/2を反復処理して、1より大きい最小の乗数を選択するだけで、それを見つけることができます。

(python)

MAX = 30
F = - math.log(2) / math.log(5)

def val(i, j):
    return 2 ** i * 5 ** j

def best(i, j):
    f = 100
    m = 0
    n = 0
    max_i = (int)(math.log(val(i, j)) / math.log(2) + 1) if i + j else 1
    #print((val(i, j), max_i, x))
    for mm in range(-i, max_i + 1):
        for rr in {-1, 0, 1}:
            nn = (int)(mm * F + rr)
            if nn < -j: continue
            ff = val(mm, nn)
            #print('  ' + str((ff, mm, nn, rr)))
            if ff > 1 and ff < f:
                f = ff
                m = mm
                n = nn
    return m, n

def detSeq():

    i = 0
    j = 0
    got = [val(i, j)]

    while len(got) < MAX:
        m, n = best(i, j)

        i += m
        j += n
        got.append(val(i, j))

        #print('* ' + str((val(i, j), m, n)))
        #print('- ' + str((v, i, j)))

    return got

これにより生成される最初の10,000の数値を、並べ替えられたリストソリューションによって生成される最初の10,000と比較して検証しましたが、少なくともそれは機能します。

ところで、1兆の次は1,024,000,000,000のようです。

...

うーん。O(n)パフォーマンス-値ごとのO(1)(!)-およびO(log n)のメモリ使用量を、best()増分的に拡張するルックアップテーブルとして扱うことで取得できます。現在は毎回繰り返すことでメモリを節約していますが、多くの冗長な計算を行っています。これらの中間値(および最小値のリスト)を保持することで、重複する作業を回避し、大幅に高速化できます。ただし、中間値のリストはnとともに増加するため、O(log n)メモリが増加します。


すばらしい答えです。コーディングしていないという同様の考えがあります。この考え方では、私は、これは最大値を追跡する2と5のためのトラッカーを維持nしてmいるが、これまで順番に番号が全体を通して使用されています。反復ごとに、nまたはm上昇しない場合があります。新しい番号を作成し2^(max_n+1)*5^(max_m+1)、現在の番号よりも大きい最小値が得られるまで、各呼び出しで指数を1ずつ減らして再帰的にこの番号を減らします。私たちは、更新max_nmax_m必要に応じて。これは一定のメモリです。O(log^2(n))DPキャッシュがリダクションコールで使用されている場合はメモリになる可能性があります
InformedA

面白い。ここでの最適化は、mとnのすべてのペアを考慮する必要がないことです。正しいm、nが1に最も近い乗数を生成することがわかっているためです。したがって、m = -iからmax_iまでを評価するだけでよく、I nを計算するだけで、丸めのためにガベージを投入できます(私はずさんで、-1から1まで繰り返しただけですが、より多くの考えを持っています;))。
Rob

しかし、私はあなたのように考えています...シーケンスは決定論的です...それは本当に、大きなパスカルの三角形の1つの方向にi + 1、もう1つの方向にj + 1のようなものです。したがって、シーケンスは数学的に決定論的でなければなりません。三角形のどのノードでも、数学的に決定された次のノードが常に存在します。
Rob

1
次のもののための式があるかもしれません、私たちは検索をする必要がないかもしれません。よくわかりません。
InformedA 2014

考えてみると、次の代数の形は存在しない可能性があります(すべての決定論的問題が解の代数の形を持っているわけではありません)。また、2と5以外の素数がある場合、式を見つけるのは非常に難しいかもしれません。本当にこの式を計算したいです。誰かがその公式を知っているなら、おそらくそれについて少し読んだ方がいいでしょう。
InformedA 2014

2

ブライアンは完全に正しかった-私の他の答えはあまりにも複雑でした。これを行うには、より簡単で高速な方法を次に示します。

整数に制限されたユークリッド平面の象限Iを想像してください。一方の軸をi軸、もう一方の軸をj軸と呼びます。

明らかに、原点に近い点は、原点から遠い点の前に選択されます。また、アクティブな領域は、j軸から離れる前にi軸から離れます。

ポイントが使用されると、それは再び使用されることはありません。また、ポイントのすぐ下または左側のポイントがすでに使用されている場合にのみ、ポイントを使用できます。

これらをまとめると、原点の周りから始まり「j」軸よりもi軸に沿って広がる「フロンティア」または「リーディングエッジ」を想像できます。

実際、さらに多くのことを理解できます。特定のi値のフロンティア/エッジに最大1つのポイントが存在します。(jの増分と等しくするには、iを2回以上増分する必要があります。)したがって、フロンティアは、j座標と関数値によってのみ変化する、各i座標の1つの要素を含むリストとして表すことができます。

各パスで、リーディングエッジの最小要素を選択してから、j方向に1回移動します。最後の要素を発生させている場合は、i値とj値が0の新しい最後の要素を追加します。

using System;
using System.Collections.Generic;
using System.Text;

namespace TwosFives
{
    class LatticePoint : IComparable<LatticePoint>
    {
      public int i;
      public int j;
      public double value;
      public LatticePoint(int ii, int jj, double vvalue)
      {
          i = ii;
          j = jj;
          value = vvalue;
      }
      public int CompareTo(LatticePoint rhs)
      {
          return value.CompareTo(rhs.value);
      }
    }


    class Program
    {
        static void Main(string[] args)
        {
            LatticePoint startPoint = new LatticePoint(0, 0, 1);

            var leadingEdge = new List<LatticePoint> { startPoint } ;

            while (true)
            {
                LatticePoint min = leadingEdge.Min();
                Console.WriteLine(min.value);
                if (min.j + 1 == leadingEdge.Count)
                {
                    leadingEdge.Add(new LatticePoint(0, min.j + 1, min.value * 2));
                }
                min.i++;
                min.value *= 5;
            }
        }
    }
}

スペース:これまでに印刷された要素数のO(n)。

速度:O(1)が挿入されますが、毎回行われるわけではありません。(時々List<>、成長する必要があるときは長くなりますが、それでもO(1)は償却されます)。大きなタイムシンクは、これまでに出力された要素数の最小値O(n)の検索です。


1
これはどのアルゴリズムを使用していますか?なぜ機能するのですか?尋ねられる質問の重要な部分Does anyone have advice on how to solve such a problem?は、根本的な問題を理解しようとすることです。コードダンプはその質問にうまく答えません。

良い点、私は私の考えを説明しました。
スノーボディ2014

+1これは2番目のスニペットとほぼ同じですが、不変エッジを使用すると、エッジ数がどのように増加するかが明確になります。
ブライアン

これは、Brianの改訂されたスニペットよりも明らかに低速ですが、メモリの使用動作は、要素を常に削除および追加するわけではないため、はるかに優れています。(CLRまたはSortedSet <>に、知らない要素を再利用するいくつかの方法がある場合を
除きます

1

セットベースのソリューションは、おそらくインタビュアーが探していたものでしたが、要素をシーケンスするためのO(n)メモリとO(n lg n)合計時間があるという残念な結果がありますn

少し計算するO(1)と、空間とO(n sqrt(n))時間のソリューションを見つけることができます。そのことに注意してください2^i * 5^j = 2^(i + j lg 5)。最初の発見nの要素は、{i,j > 0 | 2^(i + j lg 5)}最初の発見に削減nの要素{i,j > 0 | i + j lg 5}の機能があるため(x -> 2^x)、厳密に単調増加しているが、ので、いくつかのための唯一の方法であればあります。a,b2^a < 2^ba < b

ここで、がのシーケンスを見つけるアルゴリズムが必要です。i + j lg 5ここで、i,jは自然数です。つまり、現在の値がi, jである場合、次の移動を最小化する(つまり、シーケンスの次の数値を与える)のは、一方の値が(たとえばj += 1)増加し、もう一方の値が()減少することi -= 2です。私たちを制限している唯一のものはそれi,j > 0です。

考慮すべきケースは2つだけあります- i増加またはj増加。シーケンスが増加しているため、そのうちの1つは増加する必要がありますi,j。増加しないのは1つだけであるという用語をスキップするためです。したがって、1つは増加し、もう1つは同じままか減少します。C ++ 11で表現されたアルゴリズム全体と、セットソリューションとの比較は、こちらから入手できます

出力配列を除いて、メソッドに割り当てられているオブジェクトの量は一定であるため、これにより一定のメモリが実現されます(リンクを参照)。このメソッドは、与えられた(i,j)について、の値の増加が最小となる(a, b)ような最適なペアを走査するため、反復ごとに対数時間を実現(i + a, j + b)しますi + j lg 5。このトラバーサルはO(i + j)次のとおりです。

Attempt to increase i:
++i
current difference in value CD = 1
while (j > 0)
  --j
  mark difference in value for
     current (i,j) as CD -= lg 5
  while (CD < 0) // Have to increase the sequence
    ++i          // This while will end in three loops at most.
    CD += 1
find minimum among each marked difference ((i,j) -> CD)

Attempt to increase j:
++j
current difference in value CD = lg 5
while (j > 0)
  --i
  mark difference in value for
     current (i,j) as CD -= 1
  while (CD < 0) // have to increase the sequence
    ++j          // This while will end in one loop at most.
    CD += lg 5
find minimum among each marked difference ((i,j) -> CD)

すべての反復は、を更新しようと試みi、次にj、2つの更新の小さい方を更新します。

ijは最大O(sqrt(n))でなので、合計O(n sqrt(n))時間があります。iそして、jの二乗の割合で成長n任意の最大valiuesため以来imaxjmaxが存在するO(i j)私たちのシーケンスである場合は、当社のシーケンスを作るために、そこから独自のペアn用語を、とiし、j指数は線形で構成されているので、互い(のいくつかの定数倍以内に育ちます2 FOの組み合わせ)、我々はそれを知っているijしていますO(sqrt(n))

浮動小数点エラーに関して心配することはそれほど多くありません-項は指数関数的に増加するため、フロップエラーが追いつく前に、オーバーフローに対処する必要があります。時間があれば、さらに議論を加えます。


すばらしい答えです。任意の素数のシーケンスを増やすパターンもあると思います
InformedA

@randomAありがとう。さらに考えた結果、現在のところ、私のアルゴリズムは思ったほど高速ではないという結論に達しました。「i / jを増やす試み」を評価するより速い方法がある場合、それが対数時間を取得するための鍵だと思います。
VF1 2014

私はそのことを考えていました。数を増やすには、素数の1つを増やす必要があることを知っています。たとえば、増加する1つの方法は、8で乗算して5で除算することです。したがって、数を増減するすべての方法のセットを取得します。これには、mul 8 div 5のような基本的な方法のみが含まれ、mul 16 div 5は含まれません。減少する別の基本的な方法のセットもあります。これらの2つのセットを増加または減少係数でソートします。数を考えると、次は増加セットから最小の要因と該当の増加方法を見つけることによって見つけることができます...
InformedA

..適用可能とは、mulおよびdivを実行するのに十分な素数があることを意味します。次に、新しい数への減少方法を見つけます。そのため、最も減少する方法から始めます。新しい方法を使用して減少し続け、新しい数が元の所定の数よりも小さい場合に停止します。素数のセットは一定であるため、これは2つのセットのサイズが一定であることを意味します。これも少し証明が必要ですが、私には、一定の時間、各数値での一定のメモリのように見えます。したがって、定数メモリとn個の数値を印刷するための線形時間。
InformedA 2014

@randomAどこから分裂したの?完全な回答をしてもよろしいですか-コメントがよくわかりません。
VF1 2014
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.