Haskell risolve il problema 3n + 1


12

Ecco un semplice problema di programmazione da SPOJ: http://www.spoj.com/problems/PROBTRES/ .

Fondamentalmente, ti viene chiesto di produrre il più grande ciclo di Collatz per i numeri tra i e j. (Il ciclo di Collatz di un numero $ n $ è il numero di passaggi che possono eventualmente passare da $ n $ a 1.)

Ho cercato un modo Haskell per risolvere il problema con prestazioni comparative rispetto a quello di Java o C ++ (in modo da adattarsi al limite di runtime consentito). Sebbene una semplice soluzione Java che memorizza la durata del ciclo di tutti i cicli già calcolati funzionerà, non sono riuscito ad applicare l'idea di ottenere una soluzione Haskell.

Ho provato Data.Function.Memoize e la tecnica di memorizzazione dei tempi di log prodotta in casa usando l'idea di questo post: /programming/3208258/memoization-in-haskell . Sfortunatamente, la memoizzazione in realtà rende il calcolo del ciclo (n) ancora più lento. Credo che il rallentamento provenga dal sovraccarico della via di Haskell. (Ho provato a correre con il codice binario compilato, invece di interpretare.)

Ho anche il sospetto che il semplice iterare i numeri da i a j possa essere costoso ($ i, j \ le10 ^ 6 $). Quindi ho anche provato a pre-calcolare tutto per la query di intervallo, usando l'idea di http://blog.openendings.net/2013/10/range-trees-and-profiling-in-haskell.html . Tuttavia, ciò continua a generare l'errore "Superamento del limite di tempo".

Potete aiutarmi a informare un accurato programma competitivo Haskell per questo?


10
Questo post mi sembra perfetto. È un problema algoritmico che richiede una progettazione adeguata per ottenere prestazioni adeguate. Quello che davvero non vogliamo qui è domande su "come faccio a correggere il mio codice non funzionante".
Robert Harvey,

Risposte:


7

Risponderò a Scala, perché il mio Haskell non è così fresco e quindi la gente crederà che questa sia una domanda generale di algoritmo di programmazione funzionale. Seguirò strutture e concetti di dati che sono facilmente trasferibili.

Possiamo iniziare con una funzione che genera una sequenza collatz, che è relativamente semplice, tranne per la necessità di passare il risultato come argomento per renderlo ricorsivo:

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

Questo in realtà mette la sequenza in ordine inverso, ma è perfetto per il nostro prossimo passo, che è quello di memorizzare le lunghezze in una mappa:

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

Lo chiameresti con la risposta dal primo passo, la lunghezza iniziale e una mappa vuota, come calculateLengths(collatz(22), 1, Map.empty)). Questo è il modo in cui memorizzi il risultato. Ora dobbiamo modificare collatzper poter usare questo:

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

Eliminiamo il n == 1segno di spunta perché possiamo solo inizializzare la mappa con 1 -> 1, ma dobbiamo aggiungere 1le lunghezze che inseriamo nella mappa all'interno calculateLengths. Ora restituisce anche la lunghezza memorizzata in cui ha smesso di ricorrere, che possiamo usare per inizializzare calculateLengths, come:

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

Ora abbiamo implementazioni relativamente efficienti dei pezzi, dobbiamo trovare un modo per alimentare i risultati del calcolo precedente nell'input del calcolo successivo. Questo si chiama a folde assomiglia a:

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)

Ora per trovare la risposta effettiva, dobbiamo solo filtrare le chiavi nella mappa tra l'intervallo dato e trovare il valore massimo, dando un risultato finale di:

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
}

Nel mio REPL per intervalli di dimensioni pari a circa 1000, come l'input di esempio, la risposta ritorna praticamente istantaneamente.


3

Karl Bielefeld ha già risposto bene alla domanda, aggiungerò solo una versione di Haskell.

Innanzitutto una versione semplice e non memoizing dell'algoritmo di base per mostrare l'efficace ricorsione:

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)

Questo dovrebbe essere quasi autoesplicativo.

Anch'io userò un semplice Mapper memorizzare i risultati.

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

Possiamo sempre cercare i nostri risultati finali nel negozio, quindi per un singolo valore è la firma

memoCollatz :: Int -> Store -> Store

Cominciamo con il caso finale

memoCollatz 1 store = Map.insert 1 1 store

Sì, potremmo aggiungerlo in anticipo, ma non mi interessa. Prossimo caso semplice per favore.

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

Se il valore è presente, lo è. Non faccio ancora niente.

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

Se il valore non è presente, dobbiamo fare qualcosa . Mettiamo in una funzione locale. Notare come questa parte sembra molto vicina alla soluzione "semplice", solo la ricorsione è un po 'più complessa.

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

Ora finalmente facciamo qualcosa. Se troviamo il valore calcolato nel store''(sidenote: ci sono due evidenziatori della sintassi haskell, ma uno è brutto, l'altro viene confuso dal simbolo primo. Questa è l'unica ragione per il doppio-primo.), Aggiungiamo solo il nuovo valore. Ma ora diventa interessante. Se non troviamo il valore, dobbiamo sia calcolarlo sia effettuare l'aggiornamento. Ma abbiamo già funzioni per entrambi! Così

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

E ora possiamo calcolare un singolo valore in modo efficiente. Se vogliamo calcolarne diversi, passiamo semplicemente al negozio tramite una piega.

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

(È qui che è possibile inizializzare il caso 1/1.)

Ora tutto ciò che dobbiamo fare è estrarre il massimo. Per ora non ci può essere un valore nel negozio superiore a uno nell'intervallo, quindi è sufficiente dirlo

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

Ovviamente se vuoi calcolare diversi intervalli e condividere anche il negozio tra quei calcoli (le pieghe sono i tuoi amici) avresti bisogno di un filtro, ma non è l'obiettivo principale qui.


1
Per una maggiore velocità, Data.IntMap.Strictdovrebbe essere usato.
Olathe,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.