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?
algorithms
Justin Skiles
quelle
quelle
Antworten:
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:
Der Ansatz von Kilian / QuestionC ist viel performanter. C # -Schnipsel mit diesem Ansatz:
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^j
die Menge nun2^(i+1)*5^j
und enthält2^i*5^(j+1)
. Angenommen, die nächste Nummer in der Sequenz ist2^p*5^q
. Es muss eine zuvor ausgegebene Nummer des Formulars2^(p-1)*5^(q)
oder2^p*5^(q-1)
(oder beides, wenn weder p noch q gleich 0 sind) vorhanden sein. Wenn nicht, dann2^p*5^q
ist nicht die nächste Zahl, da2^(p-1)*5^(q)
und2^p*5^(q-1)
beide kleiner sind.Das zweite Snippet verwendet
O(n)
Speicher (wobei n die Anzahl der ausgegebenen Zahlen ist), daO(i+j) = O(n)
(weil i und j beide kleiner als n sind) und n Zahlen in derO(n log n)
Zeit findet. Das erste Snippet findet Zahlen in exponentieller Zeit.quelle
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
..Remove()
und.Add()
lösen ein schlechtes Verhalten des Müllsammlers aus, oder wird es etwas herausfinden?Dies ist eine häufig genug gestellte Interviewfrage, um die Antwort zu kennen. Hier ist der relevante Eintrag in meinem persönlichen Spickzettel:
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.
quelle
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 ** n
fü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)
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.quelle
n
undm
die bisher aus den Zahlen in der Sequenz verwendet wurde , durch. Bei jeder Iterationn
oderm
könnte oder könnte nicht steigen. Wir erstellen eine neue Nummer, indem wir2^(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 aktualisierenmax_n
, jemax_m
nach Bedarf. Das ist ständiges mem. KannO(log^2(n))
mem sein, wenn DP-Cache in Reduktionsaufruf verwendet wirdBrian 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.
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.quelle
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.Die satzbasierte Lösung war wahrscheinlich das, wonach Ihr Interviewer gesucht hat. Sie hat jedoch die unglückliche Folge, dass
O(n)
Speicher undO(n lg n)
Gesamtzeit für die Sequenzierung vonn
Elementen zur Verfügung stehen.Ein bisschen Mathe hilft uns, eine
O(1)
räumliche undO(n sqrt(n))
zeitliche Lösung zu finden. Beachten Sie das2^i * 5^j = 2^(i + j lg 5)
. Das Finden der erstenn
Elemente von{i,j > 0 | 2^(i + j lg 5)}
reduziert sich auf das Finden der erstenn
Elemente von,{i,j > 0 | i + j lg 5}
da die Funktion(x -> 2^x)
streng monoton ansteigt, so dass der einzige Weg für einigea,b
das2^a < 2^b
ist, wenna < b
.Nun müssen wir nur noch einen Algorithmus die Folge von zu finden
i + j lg 5
, woi,j
natürlichen Zahlen. Mit anderen Worten, angesichts unseres aktuellen Werts voni, j
ist das, was den nächsten Zug minimiert (dh uns die nächste Zahl in der Sequenz gibt), eine gewisse Zunahme eines der Werte (sagen wirj += 1
) zusammen mit einer Abnahme des anderen (i -= 2
). Das einzige, was uns einschränkt, ist dasi,j > 0
.Es sind nur zwei Fälle zu berücksichtigen -
i
Erhöhungen oderj
Erhö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 deri,j
Erhö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 isti + j lg 5
. Diese Durchquerung istO(i + j)
:Jede Iteration versucht zu aktualisieren
i
, dannj
, und geht mit dem kleineren Update des beide.Da
i
undj
höchstensO(sqrt(n))
haben wir GesamtzeitO(n sqrt(n))
.i
undj
wachsen mit der Rate des Quadratsn
für alle maximal valiues daimax
undjmax
dort existierenO(i j)
einzigartige Paare , von denen unsere Sequenz zu machen , wenn unsere Sequenzn
Bedingungen undi
undj
wachsen in einem gewissen konstanten Faktor voneinander (weil der Exponent eines linearen besteht Kombination für die beiden), das wissen wiri
undj
sind esO(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.
quelle