Drucken Sie die nächstkleinere von 2 ^ i * 5 ^ j, wobei i, j> = 0 ist

10

Diese Frage wurde mir kürzlich während eines technischen Telefon-Screenings gestellt und es ging mir nicht gut. Die Frage ist unten wörtlich enthalten.

{2^i * 5^j | i,j >= 0}Sortierte Sammlung generieren . Drucken Sie kontinuierlich den nächstkleineren Wert.

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

"Next small" lässt mich denken, dass es sich um einen Min-Heap handelt, aber ich wusste nicht wirklich, wohin ich von dort aus gehen sollte, und der Interviewer hat keine Unterstützung geleistet.

Hat jemand Ratschläge, wie man ein solches Problem löst?

Justin Skiles
quelle
Ich denke, das Interview möchte Sie bitten, es in ständiger Erinnerung zu tun. Die Verwendung von O (n) -Speicher macht dies ziemlich trivial. Oder zumindest O (logn) -Speicher verwenden, weil die Codierungsgröße für Eingabe n logn wäre. Ein O (n) für die Speicherlösung ist eine exponentielle Speicherlösung.
Informiert

Antworten:

14

Lassen Sie uns das Problem umformulieren: Geben Sie jede Zahl von 1 bis unendlich aus, sodass die Zahl keine Faktoren außer 2 und 5 enthält.

Unten ist ein einfaches C # -Schnipsel:

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);
}

Der Ansatz von Kilian / QuestionC ist viel performanter. C # -Schnipsel mit diesem Ansatz:

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 verhindert doppelte Einfügungen.

Grundsätzlich funktioniert es, indem sichergestellt wird, dass die nächste Nummer in der Sequenz in ist itms.

Beweis, dass dieser Ansatz gültig ist:
Der beschriebene Algorithmus stellt sicher, dass nach einer beliebigen Ausgabe im Formular 2^i*5^jdie Menge nun 2^(i+1)*5^jund enthält 2^i*5^(j+1). Angenommen, die nächste Nummer in der Sequenz ist 2^p*5^q. Es muss eine zuvor ausgegebene Nummer des Formulars 2^(p-1)*5^(q)oder 2^p*5^(q-1)(oder beides, wenn weder p noch q gleich 0 sind) vorhanden sein. Wenn nicht, dann 2^p*5^qist nicht die nächste Zahl, da 2^(p-1)*5^(q)und 2^p*5^(q-1)beide kleiner sind.

Das zweite Snippet verwendet O(n)Speicher (wobei n die Anzahl der ausgegebenen Zahlen ist), da O(i+j) = O(n)(weil i und j beide kleiner als n sind) und n Zahlen in der O(n log n)Zeit findet. Das erste Snippet findet Zahlen in exponentieller Zeit.

Brian
quelle
1
Hallo, Sie können sehen, warum ich während des Interviews verwirrt war, hoffe ich. Tatsächlich handelt es sich bei dem bereitgestellten Beispiel um Ausgaben aus dem in der Frage beschriebenen Satz. 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
Werden diese wiederholt .Remove()und .Add()lösen ein schlechtes Verhalten des Müllsammlers aus, oder wird es etwas herausfinden?
Schneekörper
1
@Snowbody: Die Frage des Ops ist eine Algorithmusfrage, daher ist sie etwas irrelevant. Wenn Sie dies ignorieren, sollten Sie sich zunächst mit sehr großen Ganzzahlen befassen, da dies weitaus früher zu einem Problem wird als der Overhead des Garbage Collectors.
Brian
8

Dies ist eine häufig genug gestellte Interviewfrage, um die Antwort zu kennen. Hier ist der relevante Eintrag in meinem persönlichen Spickzettel:

  • Um die Zahlen der Form 3 a 5 b 7 c der Reihe nach zu generieren , beginnen Sie mit 1, füllen Sie alle drei möglichen Nachfolger (3, 5, 7) in eine Hilfsstruktur und fügen Sie dann die kleinste Zahl daraus zu Ihrer Liste hinzu.

Mit anderen Worten, Sie benötigen einen zweistufigen Ansatz mit einem zusätzlichen sortierten Puffer, um dies effizient zu lösen. (Eine gute längere Beschreibung findet sich in Cracking the Coding Interview von Gayle McDowell.

Kilian Foth
quelle
3

Hier ist eine Antwort, die auf Kosten der CPU mit konstantem Speicher ausgeführt wird. Dies ist keine gute Antwort im Kontext der ursprünglichen Frage (dh Antwort während eines Interviews). Aber wenn das Interview 24 Stunden dauert, ist es nicht so schlimm. ;)

Die Idee ist, dass wenn ich n habe, was eine gültige Antwort ist, das nächste in der Sequenz n-mal eine Zweierpotenz sein wird, geteilt durch eine Potenz von 5. Oder n-mal eine Potenz von 5, geteilt durch a Kraft von zwei. Vorausgesetzt, es teilt sich gleichmäßig. (... oder der Divisor kann 1 sein;) In diesem Fall multiplizieren Sie nur mit 2 oder 5)

Um beispielsweise von 625 auf 640 zu gelangen, multiplizieren Sie mit 5 ** 4/2 ** 7. Oder multiplizieren Sie allgemeiner mit einem Wert von 2 ** m * 5 ** nfür einige m, n, wobei eins positiv und eins negativ oder null ist, und die Der Multiplikator teilt die Zahl gleichmäßig.

Der schwierige Teil ist nun, den Multiplikator zu finden. Wir wissen jedoch, dass a) der Divisor die Zahl gleichmäßig teilen muss, b) der Multiplikator größer als eins sein muss (die Zahlen nehmen weiter zu) und c) wenn wir den niedrigsten Multiplikator größer als 1 auswählen (dh 1 <f <alle anderen fs) ), dann ist das garantiert unser nächster Schritt. Der Schritt danach ist der niedrigste.

Der böse Teil ist das Finden des Wertes von m, n. Es gibt nur log (n) Möglichkeiten, weil es nur so viele 2er oder 5er gibt, die aufgegeben werden müssen, aber ich musste einen Faktor von -1 bis +1 hinzufügen, um schlampig mit Rundungen umzugehen. Wir müssen also nur jeden Schritt durch O (log (n)) iterieren. Es ist also insgesamt O (n log (n)).

Die gute Nachricht ist, dass Sie überall in der Sequenz beginnen können, da es einen Wert annimmt und den nächsten Wert findet. Wenn Sie also den nächsten nach 1 Milliarde möchten, können Sie ihn einfach finden, indem Sie die 2/5 oder 5/2 durchlaufen und den kleinsten Multiplikator größer als 1 auswählen.

(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

Ich habe die ersten 10.000 Zahlen, die dies generiert, gegen die ersten 10.000 validiert, die von der Lösung für sortierte Listen generiert wurden, und es funktioniert zumindest so weit.

Übrigens scheint der nächste nach einer Billion 1.024.000.000.000 zu sein.

...

Hm. Ich kann die Leistung von O (n) - O (1) pro Wert (!) - und O (log n) des Speichers erhalten, indem best()ich sie als Nachschlagetabelle behandle, die ich schrittweise erweitere. Im Moment spart es Speicher, indem es jedes Mal wiederholt wird, aber es führt viele redundante Berechnungen durch. Indem ich diese Zwischenwerte - und eine Liste von Mindestwerten - halte, kann ich die doppelte Arbeit vermeiden und sie erheblich beschleunigen. Die Liste der Zwischenwerte wächst jedoch mit n, daher der O (log n) -Speicher.

rauben
quelle
Gute Antwort. Ich habe eine ähnliche Idee, die ich nicht codiert habe. In dieser Idee halte ich einen Tracker für 2 und 5 Dadurch wird die maximale verfolgen nund mdie bisher aus den Zahlen in der Sequenz verwendet wurde , durch. Bei jeder Iteration noder mkönnte oder könnte nicht steigen. Wir erstellen eine neue Nummer, indem wir 2^(max_n+1)*5^(max_m+1)diese Nummer bei jedem Aufruf auf erschöpfende rekursive Weise reduzieren und den Exponenten um 1 reduzieren, bis wir das Minimum erhalten, das größer als die aktuelle Nummer ist. Wir aktualisieren max_n, je max_mnach Bedarf. Das ist ständiges mem. Kann O(log^2(n))mem sein, wenn DP-Cache in Reduktionsaufruf verwendet wird
InformedA
Interessant. Die Optimierung hier ist, dass nicht alle Paare von m & n berücksichtigt werden müssen, da wir wissen, dass das richtige m, n den nächsten Multiplikator zu 1 ergibt. Ich muss also nur m = -i bis max_i und I auswerten Ich kann nur n berechnen und etwas Müll für die Rundung einwerfen (ich war schlampig und habe nur -1 zu 1 iteriert, aber es muss mehr nachgedacht werden;)).
Rob
Ich denke jedoch wie Sie ... die Sequenz wird deterministisch sein ... es ist wirklich wie ein großes Pascal-Dreieck i + 1 in die eine und j + 1 in die andere Richtung. Die Reihenfolge sollte also mathematisch deterministisch sein. Für jeden Knoten im Dreieck wird es immer einen mathematisch bestimmten nächsten Knoten geben.
Rob
1
Möglicherweise gibt es eine Formel für die nächste, wir müssen möglicherweise nicht suchen. Ich weiß es nicht genau.
Informiert
Wenn ich darüber nachdenke, existiert die algebraische Form der nächsten möglicherweise nicht (nicht alle deterministischen Probleme haben eine algebraische Form für Lösungen). Wenn es mehr Primzahlen als nur 2 und 5 gibt, ist die Formel möglicherweise ziemlich schwer zu finden, wenn eine vorhanden ist will diese Formel wirklich ausarbeiten. Wenn jemand die Formel kennt, würde ich wahrscheinlich ein bisschen darüber lesen, das klingt interessant.
Informiert
2

Brian hatte absolut Recht - meine andere Antwort war viel zu kompliziert. Hier ist eine einfachere und schnellere Möglichkeit, dies zu tun.

Stellen Sie sich Quadrant I der euklidischen Ebene vor, der auf die ganzen Zahlen beschränkt ist. Nennen Sie eine Achse die i-Achse und die andere Achse die j-Achse.

Offensichtlich werden Punkte in der Nähe des Ursprungs vor Punkten ausgewählt, die weit vom Ursprung entfernt sind. Beachten Sie auch, dass sich der aktive Bereich von der i-Achse entfernt, bevor er sich von der j-Achse entfernt.

Sobald ein Punkt verwendet wurde, wird er nie wieder verwendet. Und ein Punkt kann nur verwendet werden, wenn der Punkt direkt darunter oder links davon bereits verwendet wurde.

Wenn Sie diese zusammenfügen, können Sie sich eine "Grenze" oder "Vorderkante" vorstellen, die um den Ursprung herum beginnt und sich nach oben und rechts ausbreitet und sich entlang der i-Achse weiter ausbreitet als auf der j-Achse.

Tatsächlich können wir noch etwas herausfinden: Für jeden gegebenen i-Wert gibt es höchstens einen Punkt an der Grenze / Kante. (Sie müssen i mehr als zweimal inkrementieren, um einem Inkrement von j zu entsprechen.) Wir können also die Grenze als eine Liste darstellen, die ein Element für jede i-Koordinate enthält und nur mit der j-Koordinate und dem Funktionswert variiert.

Bei jedem Durchgang wählen wir das minimale Element an der Vorderkante aus und bewegen es dann einmal in j-Richtung. Wenn wir das letzte Element erhöhen, fügen wir ein neues letztes weiteres Element mit einem inkrementierten i-Wert und einem j-Wert von 0 hinzu.

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;
            }
        }
    }
}

Leerzeichen: O (n) in Anzahl der bisher gedruckten Elemente.

Geschwindigkeit: O (1) Einfügungen, aber diese werden nicht jedes Mal ausgeführt. (Gelegentlich länger, wenn das List<>wachsen muss, aber immer noch O (1) amortisiert). Die große Zeitsenke ist die Suche nach dem Minimum O (n) in der Anzahl der bisher gedruckten Elemente.

Schneekörper
quelle
1
Welchen Algorithmus verwendet dieser? Warum funktioniert es? Der Schlüssel zu der gestellten Frage liegt Does anyone have advice on how to solve such a problem?in dem Versuch, das zugrunde liegende Problem zu verstehen. Ein Code-Dump beantwortet diese Frage nicht gut.
Guter Punkt, erklärte ich mein Denken.
Schneekörper
+1 Während dies ungefähr meinem zweiten Snippet entspricht, macht Ihre Verwendung unveränderlicher Kanten klarer, wie die Kantenanzahl wächst.
Brian
Dies ist definitiv langsamer als Brians überarbeitetes Snippet, aber sein Speichernutzungsverhalten sollte viel besser sein, da es nicht ständig Elemente löscht und hinzufügt. (Es sei denn, die CLR oder SortedSet <> hat eine Methode zur Wiederverwendung von Elementen, von denen ich nichts weiß)
Snowbody
1

Die satzbasierte Lösung war wahrscheinlich das, wonach Ihr Interviewer gesucht hat. Sie hat jedoch die unglückliche Folge, dass O(n)Speicher und O(n lg n)Gesamtzeit für die Sequenzierung von nElementen zur Verfügung stehen.

Ein bisschen Mathe hilft uns, eine O(1)räumliche und O(n sqrt(n))zeitliche Lösung zu finden. Beachten Sie das 2^i * 5^j = 2^(i + j lg 5). Das Finden der ersten nElemente von {i,j > 0 | 2^(i + j lg 5)}reduziert sich auf das Finden der ersten nElemente von, {i,j > 0 | i + j lg 5}da die Funktion (x -> 2^x)streng monoton ansteigt, so dass der einzige Weg für einige a,bdas 2^a < 2^bist, wenn a < b.

Nun müssen wir nur noch einen Algorithmus die Folge von zu finden i + j lg 5, wo i,jnatürlichen Zahlen. Mit anderen Worten, angesichts unseres aktuellen Werts von i, jist das, was den nächsten Zug minimiert (dh uns die nächste Zahl in der Sequenz gibt), eine gewisse Zunahme eines der Werte (sagen wir j += 1) zusammen mit einer Abnahme des anderen ( i -= 2). Das einzige, was uns einschränkt, ist das i,j > 0.

Es sind nur zwei Fälle zu berücksichtigen - iErhöhungen oder jErhöhungen. Einer von ihnen muss zunehmen, da unsere Sequenz zunimmt, und beide nehmen nicht zu, weil wir sonst den Begriff überspringen, in dem wir nur einen der i,jErhöhungen haben. Somit nimmt einer zu und der andere bleibt gleich oder nimmt ab. In C ++ 11 ausgedrückt, sind hier der gesamte Algorithmus und sein Vergleich mit der eingestellten Lösung verfügbar .

Dadurch wird ein konstanter Speicher erreicht, da neben dem Ausgabearray nur eine konstante Anzahl von Objekten in der Methode zugeordnet ist (siehe Link). Das Verfahren erreicht bei jeder Iteration eine logarithmische Zeit, da es für jedes gegebene (i,j)Paar das beste Paar durchläuft, (a, b)so dass dies (i + a, j + b)der kleinste Anstieg des Wertes von ist i + j lg 5. Diese Durchquerung ist 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)

Jede Iteration versucht zu aktualisieren i, dann j, und geht mit dem kleineren Update des beide.

Da iund jhöchstens O(sqrt(n))haben wir Gesamtzeit O(n sqrt(n)). iund jwachsen mit der Rate des Quadrats nfür alle maximal valiues da imaxund jmaxdort existieren O(i j)einzigartige Paare , von denen unsere Sequenz zu machen , wenn unsere Sequenz nBedingungen und iund jwachsen in einem gewissen konstanten Faktor voneinander (weil der Exponent eines linearen besteht Kombination für die beiden), das wissen wir iund jsind es O(sqrt(n)).

In Bezug auf Gleitkommafehler gibt es nicht allzu viel zu befürchten - da die Begriffe exponentiell wachsen, müssten wir uns mit Überlauf befassen, bevor Flop-Fehler uns um mehrere Größenordnungen einholen. Ich werde weitere Diskussionen hinzufügen, wenn ich Zeit habe.

VF1
quelle
Tolle Antwort, ich denke, es gibt auch ein Muster bei der Erhöhung der Sequenz für beliebige Primzahlen
InformedA
@randomA Danke. Nach einigen weiteren Überlegungen kam ich zu dem Schluss, dass mein Algorithmus derzeit nicht so schnell ist, wie ich dachte. Wenn es eine schnellere Möglichkeit gibt, "Versuch, i / j zu erhöhen" zu bewerten, ist dies meiner Meinung nach der Schlüssel, um die logarithmische Zeit zu erhalten.
VF1
Ich dachte dabei: Wir wissen, dass wir die Anzahl einer der Primzahlen erhöhen müssen, um die Anzahl zu erhöhen. Eine Möglichkeit zum Erhöhen besteht beispielsweise darin, mit 8 zu multiplizieren und durch 5 zu dividieren. Wir erhalten also alle Möglichkeiten, die Anzahl zu erhöhen und zu verringern. Dies enthält nur grundlegende Möglichkeiten wie Mul 8 Div 5 und nicht Mul 16 Div 5. Es gibt auch eine andere Reihe grundlegender Möglichkeiten zum Verringern. Sortieren Sie diese beiden Sätze nach ihrem Erhöhungs- oder Verringerungsfaktor. Wenn eine Zahl gegeben ist, kann die nächste gefunden werden, indem ein anwendbarer Erhöhungsweg mit dem kleinsten Faktor aus dem Erhöhungssatz gefunden wird.
Informiert
.. anwendbar bedeutet, dass es genügend Primzahlen gibt, um Mul und Div auszuführen. Dann finden wir einen Weg zur Verringerung der neuen Zahl, also beginnend mit dem, der am meisten abnimmt. Verwenden Sie weiterhin neue Möglichkeiten zum Verringern und wir hören auf, wenn die neue Nummer kleiner als die ursprünglich angegebene Nummer ist. Da der Satz von Primzahlen konstant ist, bedeutet dies eine konstante Größe für zwei Sätze. Dies erfordert auch ein wenig Beweis, aber es sieht für mich nach konstanter Zeit und konstantem Gedächtnis bei jeder Nummer aus. Also konstanter Speicher und lineare Zeit zum Drucken von n Zahlen.
Informiert
@randomA Woher hast du die Teilung? Stört es Sie, eine vollständige Antwort zu geben? Ich verstehe Ihre Kommentare nicht ganz.
VF1