「重複組合せ」より引用:
n 種のものから r 個取り出す組合せ(並べ方)の総数を求めよ。
ただし、同種のもの同士に区別はなく、それぞれ a1、a2、a3、・・・、an 個ずつ存在し、 n=a1+a2+a3+・・・+an≧r とする。
要するに、卑近な例を挙げれば「靴下どれでも5足で1,000円。ただしどれも在庫わずか」みたいな状況における、組み合わせの数でしょうか。こういう数のことを一般的に重複組合せ(ちょうふくくみあわせ、repeated combination)と呼び、個数の制限が無い場合は nHm=m+n-1Cm = m+n-1Cn-1 で計算することができるとのこと(数学で習ったかもしれないけど、nHmなんて記号は覚えていない)。
以下では、個数制限ありの場合の問題を解くプログラムを書いてみた。n種類の物が並んでいる中から、(a)先頭の物を1個取り出す場合、(b)取り出さない場合、のそれぞれについて関数を再帰的に適用して、和を取っていけばいいだけ。
;;; lat: 個数のリスト(a1, a2, ..., an) ;;; r: 取り出す数 (defun solve (lat r) (cond ((null lat) 0) ;リストが空なので 0 ((zerop r) 1) ;最後まで取り出したので 1 (t (+ (if (<= 1 (car lat)) ;先頭の物を取り出す場合 (solve (cons (1- (car lat)) (cdr lat)) ;先頭の物を1個消費 (1- r)) ;あと r - 1 個取り出す必要がある 0) ;在庫切れで取り出せないから 0 (solve (cdr lat) r))))) ;先頭の物を取り出さない場合 ;;; テスト (solve '(1) 1) => 1 (solve '(1 1 1) 1) => 3 ;; 以下の3通り ;; 1 0 0 ;; 0 1 0 ;; 0 0 1 (solve '(2 2 2) 3) => 7 ;; 以下の7通り ;; 2 1 0 ;; 2 0 1 ;; 1 2 0 ;; 1 1 1 ;; 1 0 2 ;; 0 2 1 ;; 0 1 2 (solve '(10 10 10) 10) => 66
最後の 66 の信憑性に関しては、こちらのページ「重複組み合わせ」に、3種類のものが10個ずつある中から10個とる場合の数は 66 だと書いてあったので間違いないと思われる。
ここでは自然に再帰で考えたけれども、漸化式を導出して動的計画法を使うと O(n×r) で解ける。具体的な方法は「Amazon.co.jp: プログラミングコンテストチャレンジブック: 秋葉 拓哉, 岩田 陽一, 北川 宜稔: 本」に書いてある。式の展開が多少面倒かもしれないが、紙と鉛筆を用意して臨めば問題ないレベルだった(数学嫌いでなければ)。