Hier ist ein einfaches Programmierproblem von SPOJ: http://www.spoj.com/problems/PROBTRES/ .
Grundsätzlich werden Sie aufgefordert, den größten Collatz-Zyklus für Zahlen zwischen i und j auszugeben. (Collatz-Zyklus einer Zahl $ n $ ist die Anzahl der Schritte, die letztendlich von $ n $ auf 1 kommen.)
Ich habe nach einer Haskell-Methode gesucht, um das Problem mit einer vergleichbaren Leistung als der von Java oder C ++ zu lösen (um das zulässige Laufzeitlimit einzuhalten). Obwohl eine einfache Java-Lösung, die die Zykluslänge aller bereits berechneten Zyklen speichert, funktioniert, konnte ich die Idee, eine Haskell-Lösung zu erhalten, nicht erfolgreich anwenden.
Ich habe das Data.Function.Memoize-Verfahren sowie das hausgemachte Protokollzeit-Memoization-Verfahren mit der Idee aus diesem Beitrag ausprobiert: /programming/3208258/memoization-in-haskell . Leider macht das Auswendiglernen die Berechnung von Zyklus (n) sogar noch langsamer. Ich glaube, die Verlangsamung kommt von der Höhe des Haskell-Weges. (Ich habe versucht, mit dem kompilierten Binärcode zu arbeiten, anstatt ihn zu interpretieren.)
Ich vermute auch, dass die einfache Iteration von Zahlen von i nach j kostspielig sein kann ($ i, j \ le10 ^ 6 $). Also habe ich sogar versucht, alles für die Bereichsabfrage vorab zu berechnen. Dabei habe ich die Idee von http://blog.openendings.net/2013/10/range-trees-and-profiling-in-haskell.html verwendet . Dies gibt jedoch immer noch den Fehler "Zeitlimitüberschreitung".
Können Sie dazu beitragen, ein ordentliches wettbewerbsfähiges Haskell-Programm zu informieren?
quelle
Antworten:
Ich werde in Scala antworten, weil mein Haskell nicht so frisch ist, und deshalb werden die Leute glauben, dass dies eine allgemeine Frage zum funktionalen Programmieralgorithmus ist. Ich werde mich an Datenstrukturen und Konzepte halten, die leicht übertragbar sind.
Wir können mit einer Funktion beginnen, die eine Kollatzsequenz erzeugt, die relativ einfach ist, außer dass das Ergebnis als Argument übergeben werden muss, damit es rekursiv wird:
Dies ordnet die Reihenfolge tatsächlich in umgekehrter Reihenfolge an, aber das ist perfekt für unseren nächsten Schritt, nämlich das Speichern der Längen in einer Karte:
Sie würden dies mit der Antwort vom ersten Schritt, der anfänglichen Länge und einer leeren Karte, wie nennen
calculateLengths(collatz(22), 1, Map.empty))
. So merken Sie sich das Ergebnis. Jetzt müssen wir modifizierencollatz
, um dies nutzen zu können:Wir eliminieren die
n == 1
Prüfung, weil wir die Karte nur mit initialisieren können1 -> 1
, aber wir müssen1
die Längen, die wir in die Karte einfügen , addierencalculateLengths
. Es gibt jetzt auch die gespeicherte Länge zurück, in der die Rekursion aufgehört hat, die wir zum Initialisieren verwenden könnencalculateLengths
, z.Da wir jetzt relativ effiziente Implementierungen der Teile haben, müssen wir einen Weg finden, die Ergebnisse der vorherigen Berechnung in die Eingabe der nächsten Berechnung einzuspeisen. Dies nennt
fold
man und sieht so aus:Um nun die eigentliche Antwort zu finden, müssen wir nur die Schlüssel in der Karte zwischen den angegebenen Bereichen filtern und den Maximalwert ermitteln, um das Endergebnis zu erhalten:
In meiner REPL für Bereiche der Größe 1000 oder so, wie in der Beispieleingabe, wird die Antwort ziemlich augenblicklich zurückgegeben.
quelle
Karl Bielefeld hat die Frage bereits gut beantwortet, ich füge nur eine Haskell-Version hinzu.
Zuerst eine einfache, nicht memoisierende Version des Basisalgorithmus, um die effiziente Rekursion zu demonstrieren:
Das sollte fast selbsterklärend sein.
Auch ich werde ein einfaches verwenden
Map
, um die Ergebnisse zu speichern.Wir können unsere Endergebnisse jederzeit im Geschäft nachschlagen, sodass für einen einzelnen Wert die Signatur lautet
Beginnen wir mit dem Endfall
Ja, wir könnten das vorher hinzufügen, aber es ist mir egal. Nächster einfacher Fall bitte.
Wenn der Wert da ist, dann ist es. Immer noch nichts zu tun.
Wenn der Wert nicht da ist, müssen wir etwas tun . Lassen Sie uns die in eine lokale Funktion setzen. Beachten Sie, dass dieser Teil der "einfachen" Lösung sehr nahe kommt, nur die Rekursion ist etwas komplexer.
Jetzt machen wir endlich was. Wenn wir den berechneten Wert in finden
store''
(Anmerkung: Es gibt zwei Hervorhebungszeichen für die Hash-Shell-Syntax, aber eines ist hässlich, das andere wird durch das Strichsymbol verwirrt. Dies ist der einzige Grund für das Doppel-Strich.), Fügen wir einfach das Neue hinzu Wert. Aber jetzt wird es interessant. Wenn wir den Wert nicht finden, müssen wir ihn berechnen und aktualisieren. Wir haben aber schon Funktionen für beide! SoUnd jetzt können wir einen einzelnen Wert effizient berechnen. Wenn wir mehrere berechnen wollen, leiten wir das Geschäft einfach über eine Falte weiter.
(Hier können Sie den 1/1 Fall initialisieren.)
Jetzt müssen wir nur noch das Maximum extrahieren. Im Moment kann es keinen Wert im Geschäft geben, der höher als einer im Sortiment ist. Es ist also genug zu sagen
Natürlich, wenn Sie mehrere Bereiche berechnen und den Speicher auch zwischen diesen Berechnungen teilen möchten (Falten sind Ihr Freund), benötigen Sie einen Filter, aber das ist hier nicht der Hauptfokus.
quelle
Data.IntMap.Strict
sollte verwendet werden.