Haskell-Wege zum 3n + 1-Problem

12

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?

haskell sieht gut aus
quelle
10
Dieser Beitrag scheint mir in Ordnung. Es ist ein algorithmisches Problem, das ein korrektes Design erfordert, um eine angemessene Leistung zu erzielen. Was wir hier wirklich nicht wollen, sind die Fragen "Wie behebe ich meinen kaputten Code?".
Robert Harvey

Antworten:

7

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:

def collatz(n: Int, result: List[Int] = List()): List[Int] = {
   if (n == 1) {
     1 :: result
   } else if ((n & 1) == 1) {
     collatz(3 * n + 1, n :: result)
   } else {
     collatz(n / 2, n :: result)
   }
 }

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:

def calculateLengths(sequence: List[Int], length: Int,
  lengths: Map[Int, Int]): Map[Int, Int] = sequence match {
    case Nil     => lengths
    case x :: xs => calculateLengths(xs, length + 1, lengths + ((x, length)))
}

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 modifizieren collatz, um dies nutzen zu können:

def collatz(n: Int, lengths: Map[Int, Int], result: List[Int] = List()): (List[Int], Int) = {
  if (lengths contains n) {
     (result, lengths(n))
  } else if ((n & 1) == 1) {
    collatz(3 * n + 1, lengths, n :: result)
  } else {
    collatz(n / 2, lengths, n :: result)
  }
}

Wir eliminieren die n == 1Prüfung, weil wir die Karte nur mit initialisieren können 1 -> 1, aber wir müssen 1die Längen, die wir in die Karte einfügen , addieren calculateLengths. Es gibt jetzt auch die gespeicherte Länge zurück, in der die Rekursion aufgehört hat, die wir zum Initialisieren verwenden können calculateLengths, z.

val initialMap = Map(1 -> 1)
val (result, length) = collatz(22, initialMap)
val newMap = calculateLengths(result, lengths, initialMap)

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 foldman und sieht so aus:

def iteration(lengths: Map[Int, Int], n: Int): Map[Int, Int] = {
  val (result, length) = collatz(n, lengths)
  calculateLengths(result, length, lengths)
}

val lengths = (1 to 10).foldLeft(Map(1 -> 1))(iteration)

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:

def answer(start: Int, finish: Int): Int = {
  val lengths = (start to finish).foldLeft(Map(1 -> 1))(iteration)
  lengths.filterKeys(x => x >= start && x <= finish).values.max
}

In meiner REPL für Bereiche der Größe 1000 oder so, wie in der Beispieleingabe, wird die Antwort ziemlich augenblicklich zurückgegeben.

Karl Bielefeldt
quelle
3

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:

simpleCollatz :: Int -> Int -> Int
simpleCollatz count 1 = count + 1
simpleCollatz count n | odd n     = simpleCollatz (count + 1) (3 * n + 1)
                      | otherwise = simpleCollatz (count + 1) (n `div` 2)

Das sollte fast selbsterklärend sein.

Auch ich werde ein einfaches verwenden Map, um die Ergebnisse zu speichern.

-- double imports to make the namespace pretty
import           Data.Map  ( Map )
import qualified Data.Map as Map

-- a new name for the memoizer
type Store = Map Int Int

Wir können unsere Endergebnisse jederzeit im Geschäft nachschlagen, sodass für einen einzelnen Wert die Signatur lautet

memoCollatz :: Int -> Store -> Store

Beginnen wir mit dem Endfall

memoCollatz 1 store = Map.insert 1 1 store

Ja, wir könnten das vorher hinzufügen, aber es ist mir egal. Nächster einfacher Fall bitte.

memoCollatz n store | Just _ <- Map.lookup n store = store

Wenn der Wert da ist, dann ist es. Immer noch nichts zu tun.

                    | odd n     = processNext store (3 * n + 1)
                    | otherwise = processNext store (n `div` 2)

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.

  where processNext store'' next | Just count <- Map.lookup next store''
                                 = Map.insert n (count + 1) store''

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! So

                                | otherwise
                                = processNext (memoCollatz next store'') next

Und jetzt können wir einen einzelnen Wert effizient berechnen. Wenn wir mehrere berechnen wollen, leiten wir das Geschäft einfach über eine Falte weiter.

collatzRange :: Int -> Int -> Store
collatzRange lower higher = foldr memoCollatz Map.empty [lower..higher]

(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

collatzRangeMax :: Int -> Int -> Int
collatzRangeMax lower higher = maximum $ collatzRange lower higher

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.

MarLinn
quelle
1
Für zusätzliche Geschwindigkeit Data.IntMap.Strictsollte verwendet werden.
Olathe