Haskellが3n + 1問題を解決する方法


12

:ここではSPOJから、簡単なプログラミングの問題であるhttp://www.spoj.com/problems/PROBTRES/が

基本的に、iとjの間の数値に対して最大のCollat​​zサイクルを出力するように求められます。(数$ n $のコラッツサイクルは、最終的に$ n $から1に到達するためのステップ数です。)

私は、JavaまたはC ++のパフォーマンスと比較してパフォーマンスを解決するHaskellの方法を探しています(許可された実行時間の制限に収まるように)。既に計算されたサイクルのサイクル長をメモする単純なJavaソリューションは機能しますが、Haskellソリューションを取得するためのアイデアの適用に成功していません。

Data.Function.Memoizeと、この投稿/programming/3208258/memoization-in-haskellのアイデアを使用して、自家製のログ時間メモ化手法を試しました。残念ながら、メモ化は実際にはcycle(n)の計算をさらに遅くします。減速はHaskellウェイのオーバーヘッドによるものだと思います。(解釈する代わりに、コンパイル済みのバイナリコードで実行してみました。)

また、iからjへの単純な反復処理にはコストがかかる($ i、j \ le10 ^ 6 $)と思われます。そこで、http://blog.openendings.net/2013/10/range-trees-and-profiling-in-haskell.htmlからのアイデアを使用して、範囲クエリのすべてを事前計算しようとしました。ただし、これでも「Time Limit Exceeding」エラーが発生します。

このために、きちんとした競争力のあるHaskellプログラムに知らせるのを手伝ってもらえますか?


10
この投稿は私にはいいようです。適切なパフォーマンスを実現するには、適切な設計が必要なアルゴリズムの問​​題です。ここで本当に望まないのは、「壊れたコードをどうやって修正するか」という質問です。
ロバートハーヴェイ

回答:


7

私のHaskellはそれほど新鮮ではないので、私はScalaで答えます。そして、人々はこれが一般的な関数型プログラミングアルゴリズムの質問であると信じるでしょう。すぐに転送できるデータ構造と概念に固執します。

collat​​zシーケンスを生成する関数から始めることができますが、結果を引数として渡して末尾を再帰的にする必要がある場合を除き、比較的簡単です。

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

これは実際にシーケンスを逆順にしますが、次のステップ、つまりマップに長さを保存するのに最適です:

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

これを呼び出すには、最初のステップからの回答、最初の長さ、およびなどの空のマップを使用しcalculateLengths(collatz(22), 1, Map.empty))ます。これが結果をメモする方法です。次に、これをcollatz使用できるように変更する必要があります。

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

n == 1地図を初期化するだけでチェックできるので、チェックを削除しますが1 -> 11内部の地図に入れる長さを追加する必要がありcalculateLengthsます。また、再帰を停止したメモされた長さも返すcalculateLengthsようになりました。

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

これで、比較的効率的なピースの実装ができました。次の計算の入力に前の計算の結果を入力する方法を見つける必要があります。これはと呼ばれ、fold次のようになります。

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)

実際の答えを見つけるには、指定された範囲の間でマップ内のキーをフィルタリングし、最大値を見つけて、最終結果を取得するだけです。

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
}

入力例のように、サイズが1000程度の範囲のREPLでは、ほぼ瞬時に答えが返されます。


3

カールビーレフェルトはすでに質問によく答えています。Haskellバージョンを追加するだけです。

最初に、効率的な再帰を自慢するための基本的なアルゴリズムの単純な非メモ化バージョン:

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)

それはほとんど自明のはずです。

私も、Map結果を保存するためにシンプルを使用します。

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

ストアで最終結果をいつでも参照できるため、単一の値の場合、署名は

memoCollatz :: Int -> Store -> Store

最後のケースから始めましょう

memoCollatz 1 store = Map.insert 1 1 store

はい、事前に追加できますが、気にしません。次の簡単なケースをお願いします。

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

値が存在する場合、存在します。まだ何もしていません。

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

値がそこになければ、何かをしなければなりません。をローカル関数に入れましょう。この部分が「単純な」ソリューションに非常によく似ていることに注目してください。再帰だけが少し複雑です。

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

今、私たちは最終的に何かをします。我々は計算値を見つけた場合store''(追記:そこ2つのHaskellの構文の蛍光ペンがありますが、一つは醜いです、もう一方は、プライム記号によって混乱してしまうのダブルプライムのための唯一の理由だこと。。)、私たちは新しいを追加値。しかし、今では面白くなっています。値が見つからない場合は、計算と更新の両方を行う必要があります。しかし、すでに両方の機能があります!そう

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

そして今、単一の値を効率的に計算できます。複数を計算したい場合は、フォールドを介してストアを渡すだけです。

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

(ここで1/1ケースを初期化できます。)

あとは、最大値を抽出するだけです。今のところ、範囲内の値よりも高い値をストアに入れることはできませんので、それで十分です。

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

もちろん、複数の範囲を計算し、それらの計算間でストアを共有する場合(フォールドは友人です)、フィルターが必要になりますが、ここでは主な焦点ではありません。


1
速度を上げるにData.IntMap.Strictは、使用する必要があります。
オレーセ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.