web-dev-qa-db-ja.com

3n + 1問題へのHaskellの方法

SPOJからの簡単なプログラミングの問題を次に示します。 http://www.spoj.com/problems/PROBTRES/

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

私は、JavaまたはC++の許容されるランタイム制限に収まるように)のパフォーマンスよりも比較パフォーマンスの問題を解決するHaskellの方法を探していました。単純なJava既に計算されたサイクルのサイクル長を記憶するソリューションは機能しますが、Haskellソリューションを得るためのアイデアを適用することに成功していません。

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

また、iからjまでの数値を単純に反復するとコストがかかる($ i、j\le10 ^ 6 $)と考えられます。ですから、 http://blog.openendings.net/2013/10/range-trees-and-profiling-in-haskell.html のアイデアを使用して、範囲クエリのすべてを事前計算してみました。ただし、これでも「制限時間超過」エラーが発生します。

これについて、きちんとした競争力のあるHaskellプログラムへの通知を手伝っていただけますか?

12

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

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 -> 1_チェックを省略しますが、calculateLengths内のマップに配置する長さに_1_を追加する必要があります。また、再帰が停止したメモの長さを返すようになりました。これを使用して、次のように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
}
_

私のREPLでは、サイズが1000程度の場合、入力例のように、答えはほとんど瞬時に返されます。

7
Karl Bielefeldt

Karl Bielefeldはすでに質問によく答えています。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)

値がない場合は、somethingを実行する必要があります。ローカル関数に入れましょう。この部分が「単純な」ソリューションに非常に似ていることに注目してください。再帰のみが少し複雑です。

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

今、私たちはついに何かをします。計算された値がstore''で見つかった場合(補足:2つのhaskell構文強調表示機能がありますが、1つは醜く、もう1つはプライムシンボルに混乱します。それがダブルプライムの唯一の理由です。)、新しい値を追加するだけです。しかし、今では面白くなっています。値が見つからない場合は、計算と更新の両方を行う必要があります。しかし、すでに両方の機能があります!そう

                                | 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

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

3
MarLinn