3n + 1 problemine Haskell yolları


12

İşte SPOJ'dan basit bir programlama problemi: http://www.spoj.com/problems/PROBTRES/ .

Temel olarak, i ve j arasındaki sayılar için en büyük Collatz döngüsünü çıkarmanız istenir. ($ N $ sayısının Collatz döngüsü, sonunda $ n $ 'dan 1' e kadar adımların atılmasıdır.)

Ben Java veya C ++ (izin verilen çalışma zamanı sınırına sığacak şekilde) daha karşılaştırmalı performans ile sorunu çözmek için bir Haskell yolu arıyordum. Zaten hesaplanmış döngülerin döngü uzunluğunu hatırlayan basit bir Java çözümü işe yarayacak olsa da, bir Haskell çözümü elde etme fikrini uygulamada başarılı olamadım.

Bu yazıdan fikri kullanarak Data.Function.Memoize yanı sıra evde demlenmiş günlük zaman memoization tekniği denedim: /programming/3208258/memoization-in-haskell . Ne yazık ki, notlaşma aslında (n) döngüsünün hesaplanmasını daha da yavaşlatır. Yavaşlamanın Haskell yolunun tepesinden geldiğine inanıyorum. (Ben yorumlamak yerine, derlenmiş ikili kod ile çalışmayı denedim.)

Ayrıca, i'den j'ye kadar olan sayıları yinelemenin maliyetli olabileceğinden şüpheleniyorum ($ i, j \ le10 ^ 6 $). Bu yüzden, http://blog.openendings.net/2013/10/range-trees-and-profiling-in-haskell.html'deki fikri kullanarak aralık sorgusu için her şeyi önceden hesaplamaya çalıştım . Ancak bu yine de "Zaman Sınırı Aşılıyor" hatası veriyor.

Bunun için düzenli bir Haskell programını bilgilendirmeye yardımcı olabilir misiniz?


10
Bu yazı bana iyi geliyor. Yeterli performansı elde etmek için uygun bir tasarıma ihtiyaç duyan algoritmik bir sorundur. Burada gerçekten istemediğimiz, "bozuk kodumu nasıl düzeltebilirim" soruları.
Robert Harvey

Yanıtlar:


7

Scala'da cevap vereceğim, çünkü Haskell'im o kadar taze değil ve insanlar bunun genel bir fonksiyonel programlama algoritması sorusu olduğuna inanacaklar. Kolayca aktarılabilen veri yapılarına ve kavramlarına bağlı kalacağım.

Sonucu kuyruk özyinelemeli hale getirmek için bir argüman olarak geçmesi gerekmesi dışında, nispeten basit olan bir collatz dizisi üreten bir işlevle başlayabiliriz:

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

Bu aslında diziyi tersine yerleştirir, ancak bu bir sonraki adımımız için mükemmeldir, bu da uzunlukları bir haritada saklamaktır:

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

Buna ilk adımdaki cevap, başlangıç ​​uzunluğu ve boş bir harita gibi ad verirsiniz calculateLengths(collatz(22), 1, Map.empty)). Sonucu bu şekilde hatırlarsınız. Şimdi bunu collatzkullanabilmek için değişiklik yapmamız gerekiyor :

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

Haritayı ortadan kaldırabileceğimiz için n == 1kontrolü kaldırıyoruz 1 -> 1, ancak 1haritayı içine koyduğumuz uzunlukları eklememiz gerekiyor calculateLengths. Biz başlatmak için kullanabileceğiniz, recursing kaldığı yerden Şimdi de memoized uzunluğunu döndürür calculateLengthsgibi:

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

Şimdi, parçaların nispeten verimli uygulamalarına sahibiz, önceki hesaplamanın sonuçlarını bir sonraki hesaplamanın girdisine beslemenin bir yolunu bulmalıyız. Buna a denir foldve şöyle görünür:

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)

Şimdi asıl cevabı bulmak için, haritadaki anahtarları verilen aralık arasında filtrelememiz ve maksimum değeri bulmamız ve sonuçların nihai bir sonucunu vermemiz gerekiyor:

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
}

Örnek girdi gibi 1000 boyut aralıkları için REPL'imde, cevap hemen hemen geri döner.


3

Karl Bielefeld soruyu çoktan yanıtladı, Haskell versiyonunu ekleyeceğim.

İlk olarak, etkin özyinelemeyi göstermek için temel algoritmanın basit, hatırlamayan bir sürümü:

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)

Bu neredeyse kendini açıklamalı olmalı.

Ben Mapde sonuçları saklamak için bir basit kullanacağım.

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

Mağazadaki nihai sonuçlarımızı her zaman arayabiliriz, böylece tek bir değer için imza

memoCollatz :: Int -> Store -> Store

Son durumla başlayalım

memoCollatz 1 store = Map.insert 1 1 store

Evet bunu önceden ekleyebiliriz, ama umrumda değil. Sonraki basit durum lütfen.

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

Değer oradaysa, odur. Hala hiçbir şey yapmıyorum.

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

Değer orada değilse, bir şeyler yapmalıyız . Yerel bir işleve koyalım. Bu parçanın "basit" çözüme çok yakın göründüğüne dikkat edin, sadece özyineleme biraz daha karmaşıktır.

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

Şimdi sonunda bir şeyler yapıyoruz. Hesaplanan değeri store''(sidenote: iki haskell sözdizimi vurgulayıcısı var, ancak biri çirkin, diğeri asal sembolle karışıyorsa. Çift prime'nin tek nedeni budur.), Sadece yeni değer. Ama şimdi ilginçleşiyor. Değeri bulamazsak, hem hesaplamak hem de güncellemeyi yapmak zorundayız. Ama zaten her ikisi için de fonksiyonlarımız var! Yani

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

Ve şimdi tek bir değeri verimli bir şekilde hesaplayabiliriz. Birkaç hesaplamak istiyorsak, mağazaya bir katlama yoluyla geçiyoruz.

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

(Burada 1/1 vakasını başlatabilirsiniz.)

Şimdi tek yapmamız gereken maksimum değeri çıkarmak. Şimdilik mağazada aralıktaki bir değerden daha yüksek bir değer olamaz, bu yüzden söylemek yeterli

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

Elbette birkaç aralığı hesaplamak ve bu hesaplamalar arasında mağazayı paylaşmak istiyorsanız (katlar arkadaşınızdır) bir filtreye ihtiyacınız olacaktır, ancak bu ana odak noktası değildir.


1
Eklenen hız Data.IntMap.Strictiçin kullanılmalıdır.
Olathe
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.