web-dev-qa-db-ja.com

これはHaskellでの順列実装を正確に何をリストしていますか?

私はData.Listモジュールのコードを研究していますが、順列のこの実装に頭を悩ませることはできません。

permutations            :: [a] -> [[a]]
permutations xs0        =  xs0 : perms xs0 []
  where
    perms []     _  = []
    perms (t:ts) is = foldr interleave (perms ts (t:is)) (permutations is)
      where interleave    xs     r = let (_,zs) = interleave' id xs r in zs
            interleave' _ []     r = (ts, r)
            interleave' f (y:ys) r = let (us,zs) = interleave' (f . (y:)) ys r
                                     in  (y:us, f (t:y:us) : zs)

これらのネストされた関数がどのように相互に接続/機能するかを誰かが詳細に説明できますか?

39
tonlika

回答が遅れて申し訳ありません。予想よりも書き留めるのに少し時間がかかりました。


したがって、まず、このようなリスト関数の遅延を最大化するには、2つの目標があります。

  • 入力リストの次の要素を検査する前に、可能な限り多くの回答を作成します
  • 答え自体は怠惰でなければならないので、同じことが成り立たなければなりません。

次に、permutation関数について考えます。ここで最大の怠惰とは、

  • 入力のn要素のみを検査した後、少なくとも_n!_順列があることを確認する必要があります
  • これらの_n!_順列のそれぞれについて、最初のn要素は、入力の最初のn要素のみに依存する必要があります。

最初の条件は次のように形式化できます

_length (take (factorial n) $ permutations ([1..n] ++ undefined))) `seq` () == ()
_

David Benbennickは2番目の条件を

_map (take n) (take (factorial n) $ permutations [1..]) == permutations [1..n] 
_

組み合わせて、

_map (take n) (take (factorial n) $ permutations ([1..n] ++ undefined)) == permutations [1..n] 
_

いくつかの単純なケースから始めましょう。最初の_permutation [1..]_。私たちは持っている必要があります

_permutations [1..] = [1,???] : ???
_

そして、2つの要素が必要です

_permutations [1..] = [1,2,???] : [2,1,???] : ???
_

最初の2つの要素の順序は選択できないことに注意してください。最初の順列は_[2,1,...]_で始まる必要があることをすでに決定しているため、最初に_1_を置くことはできません。 _permutations xs_の最初の要素がxs自体と等しい必要があることは、今では明らかです。


次に実装に進みます。

まず、リストのすべての順列を作成するには、2つの異なる方法があります。

  1. 選択スタイル:要素がなくなるまでリストから要素を選択し続けます

    _permutations []  = [[]]
    permutations xxs = [(y:ys) | (y,xs) <- picks xxs, ys <- permutations xs]
      where
        picks (x:xs) = (x,xs) : [(y,x:ys) | (y,ys) <- picks xs]
    _
  2. 挿入スタイル:可能なすべての場所に各要素を挿入またはインターリーブ

    _permutations []     = [[]]
    permutations (x:xs) = [y | p <- permutations xs, y <- interleave p]
      where
        interleave []     = [[x]]
        interleave (y:ys) = (x:y:ys) : map (y:) (interleave ys)
    _

これらのどちらも最大限に遅延しないことに注意してください。最初のケース、この関数が最初に行うことは、リスト全体から最初の要素を選択することです。 2番目のケースでは、順列を行う前に尾の順列が必要です。

まず、interleaveをより遅延させることができることに注意してください。 _interleave yss_リストの最初の要素は、_[x]_の場合は_yss=[]_または_(x:y:ys)_の場合は_yss=y:ys_です。しかし、これらはどちらも_x:yss_と同じなので、次のように書くことができます

_interleave yss = (x:yss) : interleave' yss
interleave' [] = []
interleave' (y:ys) = map (y:) (interleave ys)
_

Data.Listでの実装はこのアイデアを継続しますが、さらにいくつかのトリックを使用します。

メーリングリストの議論 をたどるのがおそらく最も簡単です。最初に、私が上記で作成したバージョンと同じ(遅延インターリーブなし)のDavid Benbennickのバージョンを使用します。 _permutations xs_の最初の要素はxs自体でなければならないことはすでに知っています。それを入れましょう

_permutations xxs     = xxs : permutations' xxs
permutations' []     = []
permutations' (x:xs) = tail $ concatMap interleave $ permutations xs
  where interleave = ..
_

tailへの呼び出しは、もちろんそれほど良くありません。しかし、permutationsinterleaveの定義をインライン化すると、

_permutations' (x:xs)
  = tail $ concatMap interleave $ permutations xs
  = tail $ interleave xs ++ concatMap interleave (permutations' xs)
  = tail $ (x:xs) : interleave' xs ++ concatMap interleave (permutations' xs)
  = interleave' xs ++ concatMap interleave (permutations' xs)
_

今私たちは持っています

_permutations xxs     = xxs : permutations' xxs
permutations' []     = []
permutations' (x:xs) = interleave' xs ++ concatMap interleave (permutations' xs)
  where
   interleave yss = (x:yss) : interleave' yss
   interleave' [] = []
   interleave' (y:ys) = map (y:) (interleave ys)
_

次のステップは最適化です。重要な目標は、インターリーブで(++)呼び出しを排除することです。最後の行map (y:) (interleave ys)のため、これはそれほど簡単ではありません。 foldr/ShowSトリックを使用して、テールをパラメーターとして渡すことはすぐにはできません。方法は、マップを取り除くことです。最後に結果にマッピングする必要がある関数としてパラメーターfを渡すと、次のようになります。

_permutations' (x:xs) = interleave' id xs ++ concatMap (interleave id) (permutations' xs)
  where
   interleave f yss = f (x:yss) : interleave' f yss
   interleave' f [] = []
   interleave' f (y:ys) = interleave (f . (y:)) ys
_

これで、尾を渡すことができます。

_permutations' (x:xs) = interleave' id xs $ foldr (interleave id) [] (permutations' xs)
  where
   interleave  f yss    r = f (x:yss) : interleave' f yss r
   interleave' f []     r = r
   interleave' f (y:ys) r = interleave (f . (y:)) ys r
_

これは、Data.Listのように見え始めていますが、まだ同じではありません。特に、それは可能な限り怠惰ではありません。試してみましょう:

_*Main> let n = 4
*Main> map (take n) (take (factorial n) $ permutations ([1..n] ++ undefined))
[[1,2,3,4],[2,1,3,4],[2,3,1,4],[2,3,4,1]*** Exception: Prelude.undefined
_

ああ、最初の_factorial n_ではなく、最初のn要素のみが正しいです。その理由は、最初の要素(上記の例では_1_)を可能な限りすべての場所に配置してから、他のことを試みるためです。


Yitzchak Galeが解決策を考え出しました。入力を最初の部分、中間要素、および尾に分割するすべての方法を検討しました。

_[1..n] == []    ++ 1 : [2..n]
       == [1]   ++ 2 : [3..n]
       == [1,2] ++ 3 : [4..n]
_

以前にこれらを生成するトリックを見たことがない場合は、Zip (inits xs) (tails xs)を使用してこれを行うことができます。これで_[1..n]_の順列は

  • _[] ++ 1 : [2..n]_別名。 _[1..n]_、または
  • _2_が_[1]_の順列のどこかに挿入(インターリーブ)され、その後に_[3..n]_が続きます。ただし、_2_は_[1]_の最後に挿入されていません。これは、前の箇条書きの結果になったためです。
  • _3_は、_[1,2]_(最後ではない)の順列にインターリーブされ、その後に_[4..n]_が続きます。
  • 等.

_3_で何かを行う前に、_[1,2]_の順列で始まるすべての順列を指定しているため、これは非常に遅延していることがわかります。 Yitzchakが与えたコードは

_permutations xs = xs : concat (zipWith newPerms (init $ tail $ tails xs)
                                                (init $ tail $ inits xs))
  where
    newPerms (t:ts) = map (++ts) . concatMap (interleave t) . permutations3
    interleave t [y]        = [[t, y]]
    interleave t ys@(y:ys') = (t:ys) : map (y:) (interleave t ys') 
_

_permutations3_の再帰呼び出しに注意してください。これは、最大限に遅延する必要がないバリアントにすることができます。

ご覧のように、これは以前より少し最適化されていません。しかし、同じトリックのいくつかを適用できます。

最初のステップは、inittailを取り除くことです。 Zip (init $ tail $ tails xs) (init $ tail $ inits xs)が実際に何であるかを見てみましょう

_*Main> let xs = [1..5] in Zip (init $ tail $ tails xs) (init $ tail $ inits xs)
[([2,3,4,5],[1]),([3,4,5],[1,2]),([4,5],[1,2,3]),([5],[1,2,3,4])]
_

initは_([],[1..n])_の組み合わせを取り除き、tailは_([1..n],[])_の組み合わせを取り除きます。前者は必要ありません。newPermsのパターンマッチが失敗するためです。後者はinterleaveに失敗します。どちらも簡単に修正できます。_newPerms []_と_interleave t []_のケースを追加するだけです。

_permutations xs = xs : concat (zipWith newPerms (tails xs) (inits xs))
  where
    newPerms [] is = []
    newPerms (t:ts) is = map (++ts) (concatMap (interleave t) (permutations is))
    interleave t []         = []
    interleave t ys@(y:ys') = (t:ys) : map (y:) (interleave t ys') 
_

これで、tailsinitsをインライン化することができます。彼らの定義は

_tails xxs = xxs : case xxs of
  []     -> []
  (_:xs) -> tails xs

inits xxs = [] : case xxs of
  []     -> []
  (x:xs) -> map (x:) (inits xs)
_

問題は、initsが末尾再帰ではないことです。ただし、いずれにしてもinitの順列をとるので、要素の順序は関係ありません。累積パラメータを使用できます

_inits' = inits'' []
  where
  inits'' is xxs = is : case xxs of
    []     -> []
    (x:xs) -> inits'' (x:is) xs
_

ここで、newPermsを、_tails xxs_および_inits xxs_ではなく、xxsおよびこの累積パラメーターの関数にします。

_permutations xs = xs : concat (newPerms' xs [])
  where
    newPerms' xxs is =
      newPerms xxs is :
      case xxs of
        []     -> []
        (x:xs) -> newPerms' xs (x:is)
    newPerms [] is = []
    newPerms (t:ts) is = map (++ts) (concatMap (interleave t) (permutations3 is))
_

newPermsを_newPerms'_にインライン化すると、

_permutations xs = xs : concat (newPerms' xs [])
  where
    newPerms' []     is = [] : []
    newPerms' (t:ts) is =
      map (++ts) (concatMap (interleave t) (permutations is)) :
      newPerms' ts (t:is)
_

concatをインライン化して展開し、最後のmap (++ts)interleaveに移動します。

_permutations xs = xs : newPerms' xs []
  where
    newPerms' []     is = []
    newPerms' (t:ts) is =
        concatMap interleave (permutations is) ++
        newPerms' ts (t:is)
      where
      interleave []     = []
      interleave (y:ys) = (t:y:ys++ts) : map (y:) (interleave ys) 
_

最後に、foldrトリックを再適用して、_(++)_を削除します。

_permutations xs = xs : newPerms' xs []
  where
    newPerms' []     is = []
    newPerms' (t:ts) is =
        foldr (interleave id) (newPerms' ts (t:is)) (permutations is)
      where
      interleave f []     r = r
      interleave f (y:ys) r = f (t:y:ys++ts) : interleave (f . (y:)) ys r
_

ちょっと待って、_(++)_を取り除くと言いました。そのうちの1つを取り除きましたが、interleaveの1つは取り除きませんでした。そのため、yysの末尾を常にtsに連結していることがわかります。したがって、interleaveの再帰とともに計算中の_(ys++ts)_を展開し、関数_interleave' f ys r_がタプル_(ys++ts, interleave f ys r)_を返すようにすることができます。これは与える

_permutations xs = xs : newPerms' xs []
  where
    newPerms' []     is = []
    newPerms' (t:ts) is =
        foldr interleave (newPerms' ts (t:is)) (permutations is)
      where
      interleave ys r = let (_,zs) = interleave' id ys r in zs
      interleave' f []     r = (ts,r)
      interleave' f (y:ys) r = 
        let (us,zs) = interleave' (f . (y:)) ys r
        in  (y:us, f (t:y:us) : zs)
_

そして、あなたはそれを手に入れました、_Data.List.permutations_。


Twanによる素晴らしい記事!私(@Yitz)はいくつかの参照を追加します:

  • Twanがこのアルゴリズムを開発した元の電子メールスレッドは、Twanによって上記にリンクされており、興味深い読み物です。

  • Knuthは、これらの条件を満たすすべての可能なアルゴリズムをVol。 4 Fasc。 2秒7.2.1.2。

  • Twanの_permutations3_は、基本的にKnuthの「アルゴリズムP」と同じです。クヌースが知る限り、そのアルゴリズムは1600年代に英国の教会のベルリンガーによって最初に公開されました。

56

基本的なアルゴリズムは、リストから一度に1つの項目を取得し、その新しい項目を含む項目のすべての順列を見つけて、繰り返すという考えに基づいています。

これがどのように見えるかを説明するために、[1 ..]は1つ上のリストを意味し、まだ値(最初のものも)はまだ調べられていません。これは関数のパラメーターです。結果のリストは次のようになります。

_[[1..]] ++
[[2,1,3..]] ++
[[3,2,1,4..], [2,3,1,4..]] ++ [[3,1,2,4..], [1,3,2,4..]]
[[4,3,2,1,5..], etc
_

上記のクラスタリングは、アルゴリズムのコアアイデアを反映しています...各行は、入力リストから取得され、並べ替えられているアイテムのセットに追加された新しいアイテムを表します。さらに、それは再帰的です...新しい行ごとに、既存のすべての順列を取得し、まだ行っていない各場所(最後以外のすべての場所)にアイテムを配置します。したがって、3行目には2つの順列[2,1]と[1,2]があり、利用可能な両方のスロットで3を実行するので、[[3,2,1]、[2,3、 1]]と[[3,1,2]、[1,3,2]]をそれぞれ入力し、監視されていない部分が何であれ追加します。

うまくいけば、これは少なくともアルゴリズムを少し明確にします。ただし、説明すべきいくつかの最適化と実装の詳細があります。

(注意:使用される2つの中心的なパフォーマンス最適化があります。最初に、複数のリストにいくつかの項目を繰り返し追加する場合、map (x:y:z:) listは、条件またはパターンマッチングよりもはるかに高速です。分岐ではなく、単に計算されたジャンプです。次に、これは頻繁に使用されます。項目を繰り返し先頭に追加することで、リストの後ろから前にリストを作成するのが安価で便利です。これは、いくつかの場所で使用されます。

関数が最初に行うことは、2つの基本ケースを確立することです。最初に、すべてのリストには少なくとも1つの順列があります。それ自体です。これは評価なしで返すことができます。これは「テイク0」のケースと考えることができます。

外側のループは、次のような部分です。

_perms (t:ts) is = <prepend_stuff_to> (perms ts (t:is))
_

tsは、リストの「変更されていない」部分であり、まだ並べ替えが行われておらず、まだ調べられていません。最初は入力シーケンス全体です。

tは、順列の間に固定される新しいアイテムです。

isは、並べ替えるアイテムのリストであり、その間にtを配置します。最初は空です。

上記の行の1つを計算するたびに、(perms ts(t:is))を含むサンクに追加した項目の最後に到達し、再帰します。


の2番目のループはフォルダーです。 is(元のリストの現在のアイテムの前のもの)の順列ごとに、アイテムをそのリストにinterleavesし、サンクの先頭に追加します。

_foldr interleave <thunk> (permutations is)
_

3番目のループは最も複雑なループの1つです。ターゲットアイテムtの可能な各散在を順列に追加し、その後に観察されないテールが結果シーケンスに続きます。これは再帰呼び出しで行われ、再帰的に順列を関数のスタックに折りたたみ、返されるときに、2つの小さな状態機械に相当するものを実行して結果を構築します。

例を見てみましょう:_interleave [<thunk>] [1,2,3]_ where _t = 4_ and _is = [5..]_

まず、interleave 'が再帰的に呼び出されると、次のようにysとfsがスタックに構築されます。

_y = 1, f = id
y = 2, f = (id . (1:))
y = 3, f = ((id . (1:)) . (2:))
(the functions are conceptually the same as ([]++), ([1]++), and ([1,2]++) respectively)
_

次に、前に戻ると、2つの値_(us, zs)_を含むタプルを返し、評価します。

usは、ターゲットyの後にtsを付加するリストです。

zsは結果アキュムレータです。新しい順列を取得するたびに、結果リストの先頭に追加します。

したがって、例を完了するために、f (t:y:us)が評価され、上のスタックの各レベルの結果として返されます。

_([1,2]++) (4:3:[5..]) === [1,2,4,3,5..]
([1]++) (4:2[3,5..])  === [1,4,2,3,5..]
([]++) (4:1[2,3,5..]) === [4,1,2,3,5..]
_

うまくいけば、それが助けになるか、少なくとも材料を補足します 上記の著者のコメントにリンクされています

(IRCでこれを取り上げ、数時間議論したことについてdfeuerに感謝します)

4
kazagistar