nCr の組合せの選び方に一意な番号を振る方法
はじめに
人間、生きていたら
どういうことかというと、たとえば A, B, C, D から2つを選ぶ組合せは
A | B | C | D | |
---|---|---|---|---|
0 |
0 | 0 | 1 | 1 |
1 |
0 | 1 | 0 | 1 |
2 |
0 | 1 | 1 | 0 |
3 |
1 | 0 | 0 | 1 |
4 |
1 | 0 | 1 | 0 |
5 |
1 | 1 | 0 | 0 |
この左側に振った 2
という番号を与えたら、それに対応する [0, 1, 1, 0]
という組合せを返してくれるような関数が欲しくなるときがありませんか?ありますよね?そう、あるんですよ。
というわけで今回作ってみました[1]。よい頭の体操になるので、皆様もぜひチャレンジしてみると面白いでしょう。
解法1:列挙して配列に保存し、インデックスで取り出す
itertools.combinations
などを使って簡単に実現できます。自分で実装するとしても再帰関数を使えば簡単です。
たとえば先ほどの
- A が 0 のときは、残りの3つから2つ選ぶ
- A が 1 のときは、残りの3つから1つ選ぶ
となるので、
一般に、
-
r == 0
:[0, 0, ..., 0]
(0が 個並ぶケース)n -
n == r
:[1, 1, ..., 1]
(1が 個並ぶケース)n
Python で実装すると以下のようになります。
def nCr(n, r):
if r == 0:
return [[0] * n]
elif n == r:
return [[1] * n]
return [ [0] + cmb for cmb in nCr(n - 1, r) ] \
+ [ [1] + cmb for cmb in nCr(n - 1, r - 1) ]
>>> nCr(4, 2)
[[0, 0, 1, 1],
[0, 1, 0, 1],
[0, 1, 1, 0],
[1, 0, 0, 1],
[1, 0, 1, 0],
[1, 1, 0, 0]]
>>> nCr(4, 2)[2]
[0, 1, 1, 0]
解法2:番号から対応する組合せを求める
というわけで少しだけ工夫する必要があるのですが、先ほど作った再帰関数について、求めたい組合せについてのみメモリに保持するよう調整すればよいです。
たとえばk
という番号(0 から数えて)に対応する組合せを求めたいときは、
このように1桁ずつ値を確定させていくようなアルゴリズムを作ればよいです。
from math import comb
def _kth_nCr(n, r, k):
"""アルゴリズム本体"""
if r == 0:
return [0] * n
elif n == r:
return [1] * n
c = comb(n - 1, r)
if k < c:
return [0] + _kth_nCr(n - 1, r, k)
return [1] + _kth_nCr(n - 1, r - 1, k - c)
def kth_nCr(n, r, k):
"""バリデーション付きのラッパー"""
c = comb(n, r)
if k >= c:
raise ValueError(f"k must be smaller than comb({n}, {r}) == {c}, but actual value {k}")
return _kth_nCr(n, r, k)
>>> kth_nCr(4, 2, 2)
[0, 1, 1, 0]
>>> [ kth_nCr(4, 2, k) for k in range(6) ]
[[0, 0, 1, 1],
[0, 1, 0, 1],
[0, 1, 1, 0],
[1, 0, 0, 1],
[1, 0, 1, 0],
[1, 1, 0, 0]]
オマケ:組合せに対応する番号を求める
『解法2』で作ったアルゴリズムと逆のことをすれば、組合せに対応する番号を一意に求めることも可能です。
from math import comb
def _index_nCr(n, r, xs):
"""アルゴリズム本体"""
if r == 0:
return 0
elif n == r:
return 0
if xs[0] == 0:
return _index_nCr(n - 1, r, xs[1:])
return comb(n - 1, r) + _index_nCr(n - 1, r - 1, xs[1:])
def index_nCr(n, r, xs):
"""バリデーション付きのラッパー"""
if any( x not in {0, 1} for x in xs ):
raise ValueError(f"invalid value detected in xs")
elif len([ x for x in xs if x == 1 ]) != r:
raise ValueError(f"number of 1 in xs must be equal to r")
return _index_nCr(n, r, xs)
>>> [ index_nCr(4, 2, cmb) for cmb in nCr(4, 2) ]
[0, 1, 2, 3, 4, 5]
>>> [ index_nCr(5, 3, cmb) for cmb in nCr(5, 3) ]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> [ index_nCr(6, 2, cmb) for cmb in nCr(6, 2) ]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
これでインデックスと組合せの間に全単射を定義することができました。
追加問題
順列に対しても同じことができるはずですので、興味のある人はやってみるとよいでしょう。
おしまい
欲求が満たされたので今回はこれでおしまいです。ありがとうございました。
-
探せばどこかに転がっているだろうと思うのですが、「組合せ」とか「番号」みたいなワードが一般的過ぎて、どう検索したら辿り着けるのか見当がつきません。ご存じでしたらぜひ教えてください。 ↩︎
Discussion