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?
sumber
Jawaban:
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:
Ini sebenarnya menempatkan urutan dalam urutan terbalik, tapi itu sempurna untuk langkah kita selanjutnya, yaitu menyimpan panjang dalam peta:
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 memodifikasicollatz
untuk dapat menggunakan ini:Kami menghilangkan tanda
n == 1
centang karena kami hanya dapat menginisialisasi peta1 -> 1
, tetapi kami perlu menambahkan1
panjang yang kami masukkan ke dalam petacalculateLengths
. Sekarang juga mengembalikan panjang memoized di mana ia berhenti berulang, yang dapat kita gunakan untuk menginisialisasicalculateLengths
, seperti: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: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:
Dalam REPL saya untuk rentang ukuran 1000 atau lebih, seperti input contoh, jawabannya kembali cukup instan.
sumber
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:
Itu harusnya hampir menjelaskan sendiri.
Saya juga akan menggunakan sederhana
Map
untuk menyimpan hasilnya.Kami selalu dapat mencari hasil akhir kami di toko, jadi untuk satu nilai tanda tangan adalah
Mari kita mulai dengan kasing akhir
Ya kita bisa menambahkan itu sebelumnya, tetapi saya tidak peduli. Kasus sederhana selanjutnya.
Jika nilainya ada, maka itu ada. Masih tidak melakukan apa-apa.
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.
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! BegituDan sekarang kita dapat menghitung nilai tunggal secara efisien. Jika kita ingin menghitung beberapa, kita hanya meneruskan toko melalui flip.
(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
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.
sumber
Data.IntMap.Strict
harus digunakan.