web-dev-qa-db-ja.com

動的計画法アルゴリズムは慣用的なHaskellでどのように実装されていますか?

Haskellやその他の関数型プログラミング言語は、状態を維持しないという前提に基づいて構築されています。関数型プログラミングのしくみとその概念についてはまだ慣れていないので、DPアルゴリズムをFPの方法で実装できるかどうか疑問に思いました。

これを行うために使用できる関数型プログラミング構造は何ですか?

43
Vanwaril

これを行う一般的な方法は、怠惰なメモ化です。ある意味で、再帰的フィボナッチ関数は、重複するサブ問題から結果を計算するため、動的計画法と見なすことができます。これは疲れた例だと思いますが、ここに味があります。怠惰なメモ化のために data-memocombinators ライブラリを使用します。

import qualified Data.MemoCombinators as Memo

fib = Memo.integral fib'
    where
    fib' 0 = 0
    fib' 1 = 1
    fib' n = fib (n-1) + fib (n-2)

fibはメモ化されたバージョンであり、fib'問題を「ブルートフォース」するだけですが、メモ化されたfibを使用してそのサブ問題を計算します。他のDPアルゴリズムは、異なるメモ構造を使用してこの同じスタイルで記述されていますが、結果を単純な機能的な方法で計算してメモ化するという同じ考え方です。

編集:私はついに諦め、記憶に残る型クラスを提供することにしました。つまり、メモ化が簡単になりました。

import Data.MemoCombinators.Class (memoize)

fib = memoize fib'
    where
    fib' :: Integer -> Integer  -- but type sig now required 
    ...

タイプに従う必要があるのではなく、memoize何でもできます。必要に応じて、古い方法を引き続き使用できます。

18
luqui

RabhiとLapalmeのアルゴリズム:関数型プログラミングアプローチには、これに関する素晴らしい章があり、いくつかのFPつまり、高階関数および遅延評価を使用します。高階関数の簡略化されたバージョンを再現しても問題ないと思います。

Intを入力として受け取り、Intを出力として生成する関数でのみ機能するという点で単純化されています。 Intは2つの異なる方法で使用しているため、「Key」と「Value」の同義語を作成します。ただし、これらは同義語であるため、キーと値を使用することは完全に可能であり、その逆も可能であることを忘れないでください。それらは読みやすさのためにのみ使用されます。

type Key = Int
type Value = Int

dynamic :: (Table Value Key -> Key -> Value) -> Key -> Table Value Key
dynamic compute bnd = t
 where t = newTable (map (\coord -> (coord, compute t coord)) [0..bnd])

この関数を少し分析してみましょう。

まず、この関数は何をしますか?型シグネチャから、何らかの形でテーブルを操作していることがわかります。実際、最初の引数「compute」はテーブルからある種の値を生成する関数(したがって、dynamicは「高階」関数)であり、2番目の引数はある種の上限であり、どこで停止するかを示します。そして出力として、「動的」関数はある種のテーブルを提供します。 DPに適した問題の答えを取得したい場合は、「動的」を実行してから、テーブルから答えを検索します。

この関数を使用してフィボナッチを計算するには、次のように実行します

fib = findTable (dynamic helper n) n
 where
  helper t i =
    if i <= 1
       then i
       else findTable t (i-1) + findTable t (i-2)

今のところ、このfib関数を理解することについてあまり心配しないでください。 「ダイナミック」を探求するにつれて、それは少し明確になります。

次に、この関数を理解するために知っておく必要のある前提条件は何ですか?構文、リストを示す[0..x]に多少なりとも精通していると思います。 0からxまで、-> Int->テーブル-> ...のような型シグネチャと\ coord-> ...のような無名関数の->これらに慣れていない場合は、仕方。

取り組むべきもう1つの前提条件は、このルックアップテーブルです。それがどのように機能するかについて心配したくはありませんが、キーと値のペアのリストからそれらを作成し、それらのエントリを検索できると仮定しましょう。

newTable :: [(k,v)] -> Table v k
findTable :: Table v k -> k -> v

ここで注意すべき3つのこと:

  • 簡単にするために、Haskell標準ライブラリの同等のものは使用していません
  • テーブルから存在しない値を検索するように要求すると、findTableがクラッシュします。必要に応じて、より洗練されたバージョンを使用してこれを回避できますが、それは別の投稿の主題です
  • 不思議なことに、本と標準のHaskellライブラリが提供しているにもかかわらず、「テーブルに値を追加する」関数については触れませんでした。何故なの?

最後に、この関数は実際にどのように機能しますか?ここで何が起こっているのですか?関数の要点を少し拡大できます。

t = newTable (map (\coord -> (coord, compute t coord)) [0..bnd])

整然とそれを引き裂きます。外から見ると、t = newTable(...)があります。これは、ある種のリストからテーブルを作成していることを示しているようです。退屈な。リストはどうですか?

map (\coord -> (coord, compute t coord)) [0..bnd]

ここでは、高階map関数がリストを0からbndまでウォークダウンし、結果として新しいリストを生成します。新しいリストを計算するには、関数\ coord->(coord、compute t coord)を使用します。コンテキストに注意してください。キーと値のペアからテーブルを作成しようとしているため、タプルを調べる場合、最初の部分の座標がキーで、2番目の部分の計算t座標が値である必要があります。その2番目の部分は、物事がエキサイティングになる場所です。もう少しズームインしてみましょう

compute t coord

キーと値のペアからテーブルを構築しており、これらのテーブルにプラグインする値は、「computetcoord」の実行から得られます。以前に言及しなかったことは、computeが入力としてテーブルとキーを受け取り、テーブルにプラグインする必要がある値、つまり、そのキーに関連付ける必要がある値を教えてくれることです。次に、これを動的計画法に戻すためのアイデアは、計算関数がテーブルの以前の値を使用して、プラグインする必要のある新しい値を計算することです。

そしてそれがすべてです! Haskellで動的計画法を実行するには、テーブルから以前の値を検索する関数を使用して値をセルに連続的にプラグインすることにより、ある種のテーブルを構築できます。簡単ですよね?...それともそうですか?

おそらくあなたは私と同じような経験をしているでしょう。ですから、この機能に取り組んでいる現在の進捗状況を共有したいと思います。この関数を最初に読んだとき、それは一種の直感的な意味を持っているようで、私はそれについてあまり考えていませんでした。それから私はそれを詳しく読んで、一種のダブルテイクをしました、何を待ちますか?!これはどのように機能するのでしょうか?ここでこのコードスニペットをもう一度見てください。

compute t coord

特定のセルの値を計算してテーブルに入力するには、最初に作成しようとしているテーブルそのものであるtを渡します。ご指摘のとおり、関数型プログラミングが不変性に関するものである場合、まだ計算されていない値を使用するこのビジネスは、どのように機能する可能性がありますか?ベルトの下にFP)が少しある場合は、私が行ったように、「それはエラーですか?」と自問するかもしれません。これは、 "地図"?

ここで重要なのは遅延評価です。それ自体のビットから不変の値を作成することを可能にするちょっとした魔法は、すべて怠惰に帰着します。一種の長期的な黄色いベルトのハスケラーなので、私はまだ怠惰の概念を少し困惑させています。だから私は他の誰かにここを引き継がせる必要があります。

その間、私はこれで大丈夫だと自分に言い聞かせます。テーブルを、たくさんの矢印が突き出た一種のドットとして視覚化することに満足しています。例としてfibを取り上げます。

o
|
|--0--> 1
|
|--1--> 1
|
|--2--> 2
|
|--3--> 2
.
.
.

私たちがまだ見たことがないテーブルの断片は、未発見の領域です。私たちが最初にリストを歩いたとき、それはすべて発見されていません

o
.
.
.

最初の値を計算する場合、i <= 1であるため、テーブルについてこれ以上知る必要はありません。

  helper t i =
    if i <= 1
       then i
       else findTable t (i-1) + findTable t (i-2)


o
|
|--0--> 1
.
.
.

連続する値を計算するときは、常にテーブルのすでに検出された部分のみを振り返っています(動的計画法、ちょっとちょっと!)。覚えておくべき重要なことは、ここでは不変の値を100%使用しており、怠惰以外に凝ったトリックはないということです。 「t」は実際にはテーブルを意味し、「反復42での現在の状態のテーブル」ではありません。実際に要求したときに、42に対応する値が何であるかを示すテーブルのビットのみを検出するだけです。

うまくいけば、StackOverflowで他の人と一緒に、あなたは私よりも先に進んで、漠然と「うーん、怠惰な何か」とつぶやくままにされないでしょう。それは本当に大したことではありません:-)

17
kowey

2つまたは3つのパラメーターでDPを使用する場合(たとえば、文字列を処理する場合)、不変の配列を使用できます。

import Data.Array.IArray

answer :: String -> Int
answer s = table ! (1, l)
  where
    l = length s

    --signatyres are needed, because GHC doesn't know what kind of Array we need
    --string is stored in Array because we need quick access to individual chars
    a :: Array Int Char
    a = listArray (1, l) s

    table :: Array (Int, Int) Int
    table = listArray ((1, 1), (l, l)) [f i j | i <- [1..l], j <- [1..l]]

    f i j |    i    >     j    = 0
          |    i    ==    j    = 1
          | (a ! i) == (a ! j) = 2 + table ! (i+1, j-1)
          | otherwise          = maximum [table ! (i+1, j), table ! (i, j-1)]

このコードは、次のタスクを解決します。文字列Sが与えられた場合、最大長のSのサブシーケンスを見つけます。これは回文になります(サブシーケンスは連続している必要はありません)。

基本的に、「f」は再帰関数であり、配列「table」はすべての可能な値の行列です。 Haskellは怠惰なので、「f」の回答値にのみ必要です。言い換えれば、これはメモ化を伴う再帰です。したがって、Data.Memocombinatorsを使用します。これはまったく同じですが、すでに他の誰かによって作成されています:)

10
Artyom

Haskellの動的計画法は怠惰のおかげでエレガントに表現できます。 このページ の最初の例を参照してください。

7
adamax

動的計画法アルゴリズムは通常、問題をより単純な問題に減らすという考えを利用します。その問題は、いくつかの基本的な事実(たとえば、正方形のセルからそれ自体への最短経路の長さが0)に加えて、問題を減らす方法を正確に示す一連の反復ルールとして定式化できます。 "セルから最短経路を見つける(i,j)から(0,0) " 問題に "セルから最短経路を見つける(i-1,j)(i,j-1)から(0,0);最高のものを選択してください」。 AFAIKこれは機能的なスタイルのプログラムで簡単に表現できます。関係する州はありません。

2
ulidtko