Rubyはほとんど使ったことがないので、コードが拙いのはご容赦ください。あと、もっと良い方法があるかもしれません。
まずは候補を[2,3,6,7]に固定したコードを書いてみましょう。
2,3,6,7のそれぞれの使用回数について、そのすべての組み合わせを考えて、足した結果が target になるものを収集すればよさそうです。
target が 7 の場合、2の使用回数は0~3回のいずれかになるはずです。(4回だと target を超えてしまう)
ここで、最大回数は target を 2 で割った結果となることに注意しましょう。
同様に3の使用回数は0~2回、6の使用回数は0~1回、7の使用回数は0~1回のいずれかになるはずです。
これらをネストしたループで繰り返して、和が target になるものを収集すると考えると、以下のようなコードになります。
([2,2,3]が[7]の前に来るように、ループは逆順に回しています)
ruby
1def combination_sum(target)
2 result = []
3 (target / 2).downto(0) do |n2|
4 (target / 3).downto(0) do |n3|
5 (target / 6).downto(0) do |n6|
6 (target / 7).downto(0) do |n7|
7 if 2 * n2 + 3 * n3 + 6 * n6 + 7 * n7 == target
8 result << [2] * n2 + [3] * n3 + [6] * n6 + [7] * n7
9 end
10 end
11 end
12 end
13 end
14 result
15end
さて、これを任意の候補でやりたいのですが、候補の数だけネストを深くしなければならないので、簡単にはいきません。
やり方としては、以下の2通りが考えられます。
- Array の product を使う
- 再帰を使う
まず 1. ですが、Array の product は、複数の配列から要素を選ぶ組み合わせのパターンをすべて列挙できます。
そのため、それぞれの候補の使用回数の配列を渡せば、1つのループで多重ループと同じことができます。
ただ、候補が1つもない場合は対応できないので別途処理する必要があります。
さらに、Array の product はやたらメモリを使うらしく、少し候補が増えると計算できなくなります。
ruby
1def combination_sum(candidates, target)
2 return (target == 0) ? [[]] : [] if candidates.empty?
3 result = []
4 as = candidates.map{|c| (target / c).downto(0).to_a}
5 a = as.shift
6 a.product(*as) do |ns|
7 if candidates.zip(ns).map{|c, n| c * n}.sum == target
8 result << candidates.zip(ns).map{|c, n| [c] * n}.sum([])
9 end
10 end
11 result
12end
次に2.の再帰を使う方法についてですが、以下のページが詳しいです。
よくやる再帰関数の書き方 〜 n 重 for 文を機械的に 〜
こちらのページに従うと、以下のようなコードになります。(面倒なのでグローバル変数を使っています)
ruby
1def combination_sum(candidates, target)
2 $candidates = candidates
3 $target = target
4 $result = []
5 dfs([])
6 $result
7end
8
9def dfs(ns)
10 if ns.size == $candidates.size
11 if $candidates.zip(ns).map{|c, n| c * n}.sum == $target
12 $result << $candidates.zip(ns).map{|c, n| [c] * n}.sum([])
13 end
14 return
15 end
16 ($target / $candidates[ns.size]).downto(0) do |n|
17 ns.push(n)
18 dfs(ns)
19 ns.pop
20 end
21end
以下は以前の回答。
再帰を使う場合は以下のようになります。
候補から合計がtargetになる組み合わせを求める際には、候補の先頭の値を使う場合と使わない場合の2つに場合分けできます。
使う場合については、先頭の値の分だけ減らしたtargetを目標とした組み合わせを求めておいて、それらの前に先頭の値を追加すれば求められます。問題の例なら、combination_sum([2,3,6,7],7-2)を求めて([[2,3]])、それぞれの先頭に2を追加します([[2,2,3]])。
使わない場合については、単純に先頭の値を除いた候補からtargetを目標とした組み合わせを求めればよいです。問題の例なら、combination_sum([3,6,7],7)を求めます([[7]])。
あとは、再帰なので終了条件を適当に決めてあげれば、以下のように書けます。
ruby
1def combination_sum(candidates, target)
2 if target == 0
3 [[]]
4 elsif target < 0 || candidates.empty?
5 []
6 else
7 head, *tail = candidates
8 combination_sum(candidates, target - head).map{|s| [head] + s} + combination_sum(tail, target)
9 end
10end
上記をボトムアップにしたり計算順を逆転させたりすると、動的計画法での解になります。
面倒なので、以下では一次元の表を上書きする形としています。
ruby
1def combination_sum(candidates, target)
2 dp = [[[]]] + [[]] * target
3 candidates.each do |c|
4 (0..target - c).each do |i|
5 dp[i + c] += dp[i].map{|s| s + [c]}
6 end
7 end
8 dp[-1]
9end
過去に同じ問題に回答していたので、リンクを貼っておきます。(ただし言語はPython)
combination sumを可読性の高いコードで計算する方法