16

数年前、私は次の問題 (またはそれに似た問題) を与えるアルゴリズムのコースを受講しました。

n一度に 2 階までしか上がらず、一度に 3 階までしか上がれないエレベータ付きの階数の建物があります。i動的計画法を使用して、エレベーターがフロアからフロアに移動するのに必要なステップ数を計算する関数を作成しますj

これは、ステートフルなアプローチを使用すると明らかに簡単です。n 要素の長さの配列を作成し、値を入力します。結果を再帰的に渡すことで結果を蓄積する、技術的に非ステートフルなアプローチを使用することもできます。私の質問は、遅延評価を使用して結び目を作ることにより、非ステートフルな方法でこれを行う方法です。


私は正しい数式を考案したと思います:

f(i,j) = 0 i が j と等しい場合 f(i,j) = 1 + f(i+2,j) と f(i-3,j) の最小値

ここでi+2、 とi-3は許容値の範囲内です。

残念ながら、私はそれを終了させることはできません。ケースを最初に置いてi+2から偶数階を選択すると、目標レベルより下の偶数階を評価することができますが、それだけです。他のすべての場合、それは最高の偶数階に向かってまっすぐに発射され、3 レベル下がってから繰り返され、上部のいくつかの階の間で永遠に振動しているのではないかと思います.

したがって、おそらく深さ優先の方法で無限空間 (または有限だがループあり) を探索しています。ステートフルなアプローチを効果的に模倣する間に大量のデータ構造を使用せずに、幅優先の方法でスペースを探索する方法を考えることはできません。


この単純な問題はがっかりするほど難しいですが、1 次元で解決策を見たので、問題の 2 次元バリエーションに対してもそれを機能させることができるのではないかと思います。


編集:多くの回答が別の方法で問題を解決しようとしました。問題自体は私にとって興味深いものではありません。問題は使用される方法に関するものです。minimal潜在的に無限の数を比較できる関数を作成する Chaosmatter のアプローチは、おそらく正しい方向への一歩です。残念ながら、100 階建ての建物を表すリストを作成しようとすると、サブ問題の解が再利用されないため、結果の計算に時間がかかりすぎます。

自己参照データ構造を使用しようとしましたが、終了しません。ある種の無限ループが発生しています。私が何をしようとしているのかを理解できるように、コードを投稿します。誰かが遅延を使用して自己参照データ構造で動的プログラミングを使用して実際に問題を解決し、複数回の計算を回避できる場合は、受け入れられた回答を変更します。

levels = go [0..10]
  where
    go [] = []
    go (x:xs) = minimum
      [ if i == 7
          then 0
          else 1 + levels !! i
        | i <- filter (\n -> n >= 0 && n <= 10) [x+2,x-3] ]
      : go xs

1 + levels !! i以前に計算された結果を参照しようとする方法と、 の値を有効なfilter (\n -> n >= 0 && n <= 10) [x+2,x-3]値に制限しようとする方法を確認できます。i私が言ったように、これは実際には機能しません。単に、この問題を解決したい方法を示しているだけです。それを解決する他の方法は私にとって興味深いものではありません。

4

4 に答える 4

9

これを 2 次元で解決しようとしており、説明されているもの以外の問題については、より一般的な解決策を検討してみましょう。有向グラフの最短経路問題を解こうとしています。

グラフの現在の表現は のようなものa -> [a]で、関数は入力から到達可能な頂点を返します。どの実装でも、2 つの頂点が同じかどうかを比較して確認できるようにする必要があるため、 が必要になりますEq a

次のグラフは問題があり、一般的に問題を解決する際のほとんどすべての困難を紹介しています。

problematic 1 = [2]
problematic 2 = [3]
problematic 3 = [2]
problematic 4 = []

1 から 4 に到達しようとすると、1 から 4 へのパスがないと判断するために検出する必要がある 2 と 3 を含むサイクルがあります。

幅優先探索

ウィルが提示するアルゴリズムは、有限グラフの一般的な問題に適用された場合、時間と空間の両方で無制限の最悪の場合のパフォーマンスを示します。サイクル検出を追加することにより、有限パスと有限サイクルのみを含むグラフの一般的な問題を攻撃するように彼のソリューションを変更できます。彼の元の解決策とこの変更は、無限グラフでも有限パスを見つけますが、無限グラフの 2 つの頂点間にパスがないと確実に判断することはできません。

acyclicPaths :: (Eq a) => (a->[a]) -> a -> a -> [[a]]
acyclicPaths steps i j = map (tail . reverse) . filter ((== j).head) $ queue
  where
    queue = [[i]] ++ gen 1 queue
    gen d _ | d <= 0 = []
    gen d (visited:t) = let r = filter ((flip notElem) visited) . steps . head $ visited 
                        in map (:visited) r ++ gen (d+length r-1) t

shortestPath :: (Eq a) => (a->[a]) -> a -> a -> Maybe [a]
shortestPath succs i j = listToMaybe (acyclicPaths succs i j)

ウィルの回答の関数を問題例の定義として再利用するstepと、11 階建ての建物の 4 階から 5 階までの最短経路の長さを で取得できますfmap length $ shortestPath (step 11) 4 5。これは を返しますJust 3

v 個の頂点と e 個の辺を持つ有限グラフを考えてみましょう。v 個の頂点と e 個の辺を持つグラフは、サイズ n ~ O(v+e) の入力によって記述できます。このアルゴリズムの最悪の場合のグラフは、到達不能な頂点 が 1 つjあり、残りの頂点と辺が から始まる最大数の非巡回パスの作成に費やされることiです。iこれはおそらく、またはではないすべての頂点を含むクリークのようなものであり、 からではない他のすべての頂点へのjエッジがあります。e 個のエッジを持つクリークの頂点の数は O(e^(1/2)) なので、このグラフは e ~ O(n), v ~ O(n^(1/2)) になります。このグラフには、O((n^(1/2))!) 個のパスがあり、到達不能であると判断する前に探索する必要があります。ijj

この場合、この関数が必要とするメモリは O((n^(1/2))!) です。これは、パスごとにキューを一定に増やすだけでよいためです。

この場合、この関数に必要な時間は O((n^(1/2))! * n^(1/2)) です。パスを展開するたびに、新しいノードがまだパスにないことを確認する必要があり、これには O(v) ~ O(n^(1/2)) の時間がかかります。訪問した頂点を保存するためにまたは同様の構造をOrd a使用した場合、これは O(log (n^(1/2))) に改善される可能性があります。Set a

非有限グラフの場合、この関数は、 からiへの有限パスが存在しないが、 からへjの非有限パスが存在する場合にのみ終了に失敗する必要があります。ij

動的計画法

動的計画法のソリューションは、同じ方法で一般化することはできません。その理由を探ってみましょう。

まず、chaosmasttter のソリューションを適応させて、幅優先探索ソリューションと同じインターフェイスを持たせます。

instance Show Natural where
    show = show . toNum 

infinity = Next infinity

shortestPath' :: (Eq a) => (a->[a]) -> a -> a -> Natural
shortestPath' steps i j = go i
    where
        go i | i == j = Zero
             | otherwise = Next . foldr minimal infinity . map go . steps $ i

これは、エレベータの問題でうまく機能しshortestPath' (step 11) 4 5ます3。残念ながら、問題のある問題でshortestPath' problematic 1 4は、スタックがオーバーフローします。Natural数値のコードをもう少し追加すると、次のようになります。

fromInt :: Int -> Natural
fromInt x = (iterate Next Zero) !! x    

instance Eq Natural where
    Zero == Zero         = True
    (Next a) == (Next b) = a == b
    _ == _ = False

instance Ord Natural where
    compare Zero Zero         = EQ
    compare Zero _            = LT
    compare _ Zero            = GT
    compare (Next a) (Next b) = compare a b

最短経路が上限よりも短いかどうかを尋ねることができます。私の意見では、これは遅延評価で何が起こっているかを実際に示しています。problematic 1 4 < fromInt 100です。False_ problematic 1 4 > fromInt 100_True

次に、動的計画法を調べるために、動的計画法を導入する必要があります。すべての下位問題に対する解の表を作成するため、頂点が取り得る可能な値を知る必要があります。これにより、わずかに異なるインターフェースが得られます。

shortestPath'' :: (Ix a) => (a->[a]) -> (a, a) -> a -> a -> Natural
shortestPath'' steps bounds i j = go i
    where
        go i = lookupTable ! i
        lookupTable = buildTable bounds go2
        go2 i | i == j = Zero
              | otherwise = Next . foldr minimal infinity . map go . steps $ i

-- A utility function that makes memoizing things easier
buildTable :: (Ix i) => (i, i) -> (i -> e) -> Array i e
buildTable bounds f = array bounds . map (\x -> (x, f x)) $ range bounds

shortestPath'' (step 11) (1,11) 4 5これはまたはのように使用できますshortestPath'' problematic (1,4) 1 4 < fromInt 100。これはまだサイクルを検出できません...

動的計画法とサイクル検出

サブ問題は、異なるパスからアプローチされた場合に同じではないため、動的計画法ではサイクル検出が問題になります。problematic私たちの問題の変形を考えてみましょう。

problematic' 1 = [2, 3]
problematic' 2 = [3]
problematic' 3 = [2]
problematic' 4 = []

1からに移動しようとしている場合4、次の 2 つのオプションがあります。

  • に行き、からへ2の最短経路を取る24
  • に行き、からへ3の最短経路を取る34

探索することを選択した場合2、次のオプションに直面します。

  • に行き、からへ3の最短経路を取る34

3からへの最短経路の 2 つの探索を4テーブルの同じエントリに結合したいと考えています。サイクルを回避したい場合、これは実際にはもう少し微妙なことです。私たちが実際に直面した問題は次のとおりです。

  • に行ってから、訪問しない2最短経路を2たどる41
  • に行ってから、訪問しない3最短経路を3たどる41

選択後2

  • に行き、そこからへ3の最短経路をたどる3412

2 つのわずかに異なる答え3を得る方法についてのこれらの 2 つの質問。4これらは、テーブルの同じ場所に収まらない 2 つの異なるサブ問題です。4最初の質問に答えるには、最終的にfromに到達できないと判断する必要があります2。2 番目の質問への回答は簡単です。

以前に訪れた頂点の可能なセットごとに一連のテーブルを作成することもできますが、それはあまり効率的ではないようです。遅延のみを使用した動的プログラミングの問題として到達可能性を実行することはできないと、私はほとんど確信しています。

幅優先探索還元

到達可能性またはサイクル検出を使用した動的計画法ソリューションに取り組んでいるときに、オプションでノードを確認すると、そのノードをたどるかどうかに関係なく、そのノードにアクセスするパスが最適になることはないことに気付きました。再考するとproblematic'

1からに移動しようとしている場合4、次の 2 つのオプションがあります。

  • に行き、 、、またはを訪問せずにからへ2の最短経路を取る24123
  • に行き、 、、またはを訪問せずにからへ3の最短経路を取る34123

これにより、最短経路の長さを非常に簡単に見つけるアルゴリズムが得られます。

-- Vertices first reachable in each generation
generations :: (Ord a) => (a->[a]) -> a -> [Set.Set a]
generations steps i = takeWhile (not . Set.null) $ Set.singleton i: go (Set.singleton i) (Set.singleton i)
    where go seen previouslyNovel = let reachable = Set.fromList (Set.toList previouslyNovel >>= steps)
                                        novel = reachable `Set.difference` seen
                                        nowSeen = reachable `Set.union` seen
                                    in novel:go nowSeen novel

lengthShortestPath :: (Ord a) => (a->[a]) -> a -> a -> Maybe Int
lengthShortestPath steps i j = findIndex (Set.member j) $ generations steps i

さすが、lengthShortestPath (step 11) 4 5です。_Just 3lengthShortestPath problematic 1 4Nothing

最悪の場合、generationsO(v*log v) のスペースと O(v*e*log v) の時間が必要になります。

于 2013-11-23T17:53:20.427 に答える
9

問題は、へのmin両方の呼び出しを完全に評価する必要があるfため、そのうちの 1 つが無限にループするminと決して返されないことです。fしたがって、返される数値がゼロまたはゼロの後継者であることをエンコードする新しい型を作成する必要があります。

data Natural = Next Natural 
             | Zero

toNum :: Num n => Natural -> n
toNum Zero     = 0
toNum (Next n) = 1 + (toNum n)

minimal :: Natural -> Natural -> Natural
minimal Zero _            = Zero
minimal _ Zero            = Zero
minimal (Next a) (Next b) = Next $ minimal a b

f i j | i == j = Zero
      | otherwise = Next $ minimal (f l j) (f r j)
      where l = i + 2
            r = i - 3

このコードは実際に機能します。

于 2013-11-23T12:01:34.023 に答える
4

階の建物の床に立って、床in着くのに必要な最小の歩数を見つけてくださいj。ここで

step n i = [i-3 | i-3 > 0] ++ [i+2 | i+2 <= n]

したがって、ツリーがあります。値を保持するノードを取得するまで、幅優先で検索する必要がありますj。その深さはステップ数です。深度レベルを運ぶキューを構築し、

solution n i j = case dropWhile ((/= j).snd) queue
                   of []        -> Nothing
                      ((k,_):_) -> Just k
  where
    queue = [(0,i)] ++ gen 1 queue

この関数は、ノッチからgen d pの入力を、出力キューに沿った生産点から戻します。pd

    gen d _ | d <= 0 = []
    gen d ((k,i1):t) = let r = step n i1 
                       in map (k+1 ,) r ++ gen (d+length r-1) t

TupleSectionsを使用します。ここには結び目はありません。コアカーション、つまり (楽観的な) フォワード プロダクションと倹約的な探索だけです。最初の解のみを探すため、結び目を作らなくても問題なく動作します。それらのいくつかを検索している場合は、何らかの方法でサイクルを排除する必要があります.

サイクル検出では:

solutionCD1 n i j = case dropWhile ((/= j).snd) queue
                    of []        -> Nothing
                       ((k,_):_) -> Just k
  where
    step n i visited =    [i2 | let i2=i-3, not $ elem i2 visited, i2 > 0] 
                       ++ [i2 | let i2=i+2, not $ elem i2 visited, i2 <=n]
    queue = [(0,i)] ++ gen 1 queue [i]
    gen d _ _ | d <= 0 = []
    gen d ((k,i1):t) visited = let r = step n i1 visited
                               in map (k+1 ,) r ++ 
                                  gen (d+length r-1) t (r++visited)

たとえばsolution CD1 100 100 7、即座に実行され、生成されJust 31ます。visitedリストは、キュー自体のインスタンス化されたプレフィックスのほとんどのコピーです。時間の複雑さを改善するために、マップとして維持することができます (そのままでは、sol 10000 10000 7 => Just 3331Ideone で 1.27 秒かかります)。


いくつかの説明は適切なようです。

まず、ターゲット フロアjが固定されているため、問題に 2D はありません。

最新の編集が示すように、あなたが望んでいるように見えるのはmemoizationです。メモ化は再帰的なソリューションに役立ちます。あなたの関数は確かに再帰的です - その引数をサブケースに分析し、ベースケース (ここでは ) に近いサブケース (ここではi+2と) で自分自身を呼び出した結果から結果を合成します。i-3i==j

算術演算は厳密であるため、ステップ ツリーに無限パス (フロアからフロアへの移動) が存在する場合、式は発散します。代わりに遅延演算を使用することによる chaosmasttter による答えは、上記の最初のソリューションとまったく同じように、ツリーに有限パスがない場合にのみ発散する幅優先探索アルゴリズムに自動的に変換します (チェックしていないという事実を保存してください)範囲外のインデックス)。しかし、それでも再帰的であるため、実際にはメモ化が必要です。

最初にアプローチする通常の方法は、 「リストをたどる」ことで共有を導入することです (シーケンシャル アクセスのため非効率的です。効率的なメモ化ソリューションについては hackage を参照してください)。

f n i j = g i
  where
    gs = map g [0..n]              -- floors 1,...,n  (0 is unused)
    g i | i == j = Zero
        | r > n  = Next (gs !! l)  -- assuming there's enough floors in the building
        | l < 1  = Next (gs !! r)
        | otherwise = Next $ minimal (gs !! l) (gs !! r)
      where r = i + 2
            l = i - 3

未検証。

私の解決策はcorecursiveです。動的計画法と同様に生成的であるため、メモ化は必要ありません (重複に注意する必要があるだけです)。開始ケース、つまり開始フロアから離れます。外部アクセサーは、生成された適切な結果を選択します。

それは結び目を結びます-それqueueを使用して定義します-queue方程式の両側にあります。以前に生成された値に変装してアクセスするだけなので、結び目を結ぶより単純なケースだと思います。

より複雑な 2 番目の種類の結び目結びは、通常、まだ定義されていない値を何らかのデータ構造に入れ、それをコードの後半部分で定義されるように戻すことです (たとえば、二重のバックリンク ポインターなど)。リンクされた循環リスト); これは実際、私の1コードが行っていることではありません。それが行うことは、queueを生成し、その最後に追加し、その前から「削除」することです。結局のところ、これは Prolog の差分リスト手法であり、エンド ポインターが維持および更新された制限のないリストであり、cons を法とする末尾再帰のトップダウン リスト構築です。概念的にはすべて同じです。1974年に最初に説明されました(名前はありませんが)、AFAIK。


1完全にWikipediaのコードに基づいています。

于 2013-11-23T11:59:55.717 に答える