web-dev-qa-db-ja.com

配列のすべての可能なサブ配列を見つける

私は迷っています。バックトラック/再帰アプローチに頭を悩ませているようです。階乗のような単純な再帰問題がどのように機能するかを理解しているので、手動で追跡することもできます。しかし、それがバックトラックの問題になると、私はとても迷っています。頑張ります。 5時間経ちましたが、フォーラムやスタック交換に関するさまざまなアプローチを読んでいますが、何もクリックしていません。

たとえば、[1,2,3,4]の配列があり、宛先の値が5であるとします。目的の値に等しいすべての可能な組み合わせを見つけようとしています。

私はこの問題をさらに詳しく分析して、自分が理解しやすいようにしました。最初に、配列の可能なすべての組み合わせを見つけて、それらを別の関数に渡し、その配列の合計が宛先値と等しいかどうかをチェックして、それを出力するだけです。

誰かがこれに取り組む方法をアドバイスできますか?私は頭の中でこれをはっきりと想像し、想像できるようにしたいコード/答えを探していません。

4
user2733436

サイズnの配列を取ります。あそこ2 この配列の可能なサブ配列。サイズ4のサンプル配列を見てみましょう:[1, 2, 3, 4]。 2つあります4 サブ配列。

空のセットのサブ配列([])は0です番目 1つ(0000)。 [1]のサブ配列は2番目のもの(0001)であり、[2]のサブ配列は2番目のものです...(0010)およびサブ配列[1, 2]は3番目(0011)です。これがどこに向かっているのかがわかるはずです。

これには再帰は必要ありません。サブ配列のセットは、nのバイナリ表現です番目 0から2の範囲の値 -1。

この実現により、特定の配列のすべてのサブ配列の生成は、整数を反復し、特定の要素がサブ配列内にあるかどうかのビットフィールドとしてそれらを見るという簡単な問題になるはずです。

すべてのkkの組み合わせの数も参照 ウィキペディア。


これはおそらくCや類似の言語でそれを行うための正しい方法であることを指摘します。再帰、リスト、バックトラックを強制し、そうでなければ、明確で理解可能な答えを持つ単純な反復問題になることを試みるのは、最善のアプローチではないかもしれません。

これに対する他の提案された回答はすぐに関数型プログラミングの例とソリューションになることに注意してください。関数型プログラミングは素晴らしいです。ただし、このようなプログラミングのパラダイムを強制しようとするのに適していない言語では、LISPでCコードを作成するのと同じです。問題に取り組むための最良の方法ではない可能性があります。

さらに、この質問で示唆されている問題は次のとおりであることを指摘しておきます。

たとえば、[1,2,3,4]の配列があり、宛先の値が5であるとします。宛先の値と等しいすべての可能な組み合わせを見つけようとしています。

これは サブセット合計問題 として知られているよく知られた問題であり、それを解決するためのいくつかのアプローチがあります...配列のすべてのサブセットを生成することは、より高速なアプローチには必要ありません(または望まれさえしません)それを解決するために。

5
user40980

合計して1 :: 2 :: 3 :: 4 :: []になる5のサブリストのセットを再帰的に列挙したいとします。 (::は、リスト構築のOCaml表記です。)リスト処理の再帰的なステップは、リストの先頭1と末尾2 :: 3 :: 4 :: []を分割することです—セットをこれらの用語?

1 :: 2 :: 3 :: 4 :: []には、合計で5となる2種類のサブリストがあります。

  • 最初の種類は、1で始まり、合計が4 = 5 - 1になるリストが続くリストで構成されます。
  • 2番目の種類は、2 :: 3 :: 4 :: []から5までのサブリストで構成されます。

素晴らしい! partition関数int list -> int -> int list listを呼び出して、必要なすべてのサブリストを見つける場合、上記の説明は、リストのpartition lstを定義する方法を説明しただけですlstof length n長さn-1のリストの定義が与えられています。

OCamlで関数を書くのは簡単です:

let rec partition lst w =
  if w = 0 then
    [[]]
  else
    match lst with
      | [] -> []
      | hd :: tl -> firstkind hd tl (w - hd) @ partition tl w
and firstkind hd tl w =
  List.map (fun lst -> hd :: lst) (partition tl w)

この演習では、繰り返しを必要としないことを前提としています。たとえば、[1..4]の組み合わせは次のとおりです。

_[[1],[1,2],[1,2,3],[1,2,3,4],[1,2,4],[1,3],[1,3,4],
 [1,4],[2],[2,3],[2,3,4],[2,4],[3],[3,4],[4]]
_

そうでない場合、プロセスはすべての再帰関数で実際に同じです。基本ケースを理解し、小さな問題の観点から大きな問題を理解します。

リストを取り、リストのリストを返す関数subarraysを作成します。すべての組み合わせに対して1つなので、次のようになります。

_subarrays :: [a] -> [[a]]
_

ベースケースは通常非常に簡単です。この場合、空のリストの部分配列は空のリストなので、次のようになります。

_subarrays [] = []
_

リスト関数の再帰的なステップは、ほとんどの場合、「ヘッド」と呼ばれる最初の要素(ここではxと表記)と、「テール」と呼ばれる残りの部分(ここではxsと表記)に分割します。尾の正しい答えはすでにわかっていると想定し、それを使用して頭の答えを作成します。ここで、記号combosを尾の答えに割り当てます。

_subarrays (x:xs) = let combos = subarrays xs
_

これまでのところ、これは比較的定型的なコードであり、ほぼすべての再帰関数で何らかの形で見つかります。トリッキーな部分は、リストを小さくするために解決したことです。別のレイヤーを追加するにはどうすればよいですか?

例として、combosに[2,3,4]のすべてのソリューションが含まれていて、_1_を含むソリューションも含まれているリストを作成しているとします。この場合、3つのことのいずれかを行って、より大きなリストを作成できます。

  • _1_を単独で使用するだけです。 (_[x]_)
  • 他のすべてのコンボを自分で持っているだけです。 (combos
  • 他のすべてのコンボに_1_を追加します。 (map (x:) combos

次に、これらすべての可能性を含むリストを返します。

_[x] : map (x:) combos ++ combos
_

これですべてです。以下は、合計テスト関数を含むHaskellの完全なプログラムです。

_subarrays :: [a] -> [[a]]
subarrays [] = []
subarrays (x:xs) = let combos = subarrays xs
                   in [x] : map (x:) combos ++ combos

combosSumTo :: Int -> [Int] -> [[Int]]
combosSumTo x = filter (\y -> sum y == x) . subarrays
_

私がどのように3つのことを思いついたのかに関しては、練習して、テストして繰り返す必要があります。初めてこれを書いたとき、誤って_[x]_の部分を忘れてしまいましたが、テストでそれが表示されました。トリックはあなたの頭の中のスタックを再帰しようとしないことです。再帰呼び出しがリストの末尾の正しい答えを与えると仮定してください。

1
Karl Bielefeldt