Haskell cara untuk masalah 3n +1


12

Berikut ini adalah masalah pemrograman sederhana dari SPOJ: http://www.spoj.com/problems/PROBTRES/ .

Pada dasarnya, Anda diminta untuk menampilkan siklus Collatz terbesar untuk angka antara i dan j. (Siklus collatz dari sejumlah $ n $ adalah jumlah langkah untuk akhirnya mendapatkan dari $ n $ ke 1.)

Saya telah mencari cara Haskell untuk memecahkan masalah dengan kinerja komparatif daripada Java atau C ++ (sehingga sesuai dengan batas run-time yang diizinkan). Meskipun solusi Java sederhana yang memoise panjang siklus dari setiap siklus yang sudah dihitung akan berhasil, saya belum berhasil menerapkan ide untuk mendapatkan solusi Haskell.

Saya telah mencoba Data.Function.Memoize, serta teknik memoisasi waktu log buatan sendiri menggunakan ide dari pos ini: /programming/3208258/memoization-in-haskell . Sayangnya, memoisasi justru membuat perhitungan siklus (n) lebih lambat. Saya percaya perlambatan datang dari overhead cara Haskell. (Saya mencoba menjalankan dengan kode biner yang dikompilasi, bukannya menafsirkan.)

Saya juga menduga bahwa hanya iterasi angka dari i ke j dapat mahal ($ i, j \ le10 ^ 6 $). Jadi saya bahkan mencoba precompute semuanya untuk kueri rentang, menggunakan ide dari http://blog.openendings.net/2013/10/range-trees-and-profiling-in-haskell.html . Namun, ini masih memberikan kesalahan "Batas Waktu Melebihi".

Bisakah Anda membantu menginformasikan program Haskell yang kompetitif dan rapi untuk ini?


10
Posting ini sepertinya baik untuk saya. Ini masalah algoritmik yang membutuhkan desain yang tepat untuk mencapai kinerja yang memadai. Yang benar-benar tidak kita inginkan di sini adalah pertanyaan "bagaimana cara memperbaiki kode saya yang rusak".
Robert Harvey

Jawaban:


7

Saya akan menjawab dalam Scala, karena Haskell saya tidak segar, sehingga orang akan percaya ini adalah pertanyaan algoritma pemrograman fungsional umum. Saya akan berpegang teguh pada struktur dan konsep data yang siap dipindahtangankan.

Kita bisa mulai dengan fungsi yang menghasilkan urutan collatz, yang relatif mudah, kecuali harus melewati hasil sebagai argumen untuk membuatnya rekursif:

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

Ini sebenarnya menempatkan urutan dalam urutan terbalik, tapi itu sempurna untuk langkah kita selanjutnya, yaitu menyimpan panjang dalam peta:

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

Anda akan menyebutnya dengan jawaban dari langkah pertama, panjang awal, dan peta kosong, seperti calculateLengths(collatz(22), 1, Map.empty)). Ini adalah bagaimana Anda memotret hasilnya. Sekarang kita perlu memodifikasi collatzuntuk dapat menggunakan ini:

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

Kami menghilangkan tanda n == 1centang karena kami hanya dapat menginisialisasi peta 1 -> 1, tetapi kami perlu menambahkan 1panjang yang kami masukkan ke dalam peta calculateLengths. Sekarang juga mengembalikan panjang memoized di mana ia berhenti berulang, yang dapat kita gunakan untuk menginisialisasi calculateLengths, seperti:

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

Sekarang kami memiliki implementasi potongan yang relatif efisien, kami perlu menemukan cara untuk memberi makan hasil perhitungan sebelumnya ke dalam input perhitungan selanjutnya. Ini disebut fold, dan terlihat seperti:

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)

Sekarang untuk menemukan jawaban yang sebenarnya, kita hanya perlu menyaring kunci di peta antara rentang yang diberikan, dan menemukan nilai maks, memberikan hasil akhir dari:

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
}

Dalam REPL saya untuk rentang ukuran 1000 atau lebih, seperti input contoh, jawabannya kembali cukup instan.


3

Karl Bielefeld telah menjawab pertanyaan dengan baik, saya hanya akan menambahkan versi Haskell.

Pertama, sederhana, versi non-memoizing dari algoritma dasar untuk memamerkan rekursi efisien:

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)

Itu harusnya hampir menjelaskan sendiri.

Saya juga akan menggunakan sederhana Mapuntuk menyimpan hasilnya.

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

Kami selalu dapat mencari hasil akhir kami di toko, jadi untuk satu nilai tanda tangan adalah

memoCollatz :: Int -> Store -> Store

Mari kita mulai dengan kasing akhir

memoCollatz 1 store = Map.insert 1 1 store

Ya kita bisa menambahkan itu sebelumnya, tetapi saya tidak peduli. Kasus sederhana selanjutnya.

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

Jika nilainya ada, maka itu ada. Masih tidak melakukan apa-apa.

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

Jika nilainya tidak ada, kita harus melakukan sesuatu . Mari kita masukkan fungsi lokal. Perhatikan bagaimana bagian ini terlihat sangat dekat dengan solusi "sederhana", hanya rekursi yang sedikit lebih kompleks.

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

Sekarang kami akhirnya melakukan sesuatu. Jika kita menemukan nilai yang dikomputasi dalam store''(sidenote: ada dua penyorot sintaksis haskell, tetapi satu jelek, yang lain menjadi bingung oleh simbol utama. Itulah satu-satunya alasan untuk double-prime.), Kita tambahkan saja yang baru nilai. Tapi sekarang menjadi menarik. Jika kami tidak menemukan nilai, kami harus menghitungnya dan melakukan pembaruan. Tapi kami sudah memiliki fungsi untuk keduanya! Begitu

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

Dan sekarang kita dapat menghitung nilai tunggal secara efisien. Jika kita ingin menghitung beberapa, kita hanya meneruskan toko melalui flip.

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

(Di sinilah Anda dapat menginisialisasi kasing 1/1.)

Sekarang yang harus kita lakukan adalah mengekstraksi secara maksimal. Untuk saat ini tidak mungkin ada nilai di toko yang lebih tinggi dari satu di kisaran, jadi itu sudah cukup untuk dikatakan

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

Tentu saja jika Anda ingin menghitung beberapa rentang dan berbagi toko di antara perhitungan itu juga (lipatan adalah teman Anda), Anda akan memerlukan filter, tapi itu bukan fokus utama di sini.


1
Untuk menambah kecepatan, Data.IntMap.Strictharus digunakan.
Olathe
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.