なんて素晴らしい質問でしょう!用語と一致させるために、マトリックス内の行を「入力バッグ」と呼び、入力バッグ内の項目を「オブジェクト」と呼びます。バッグ (またはマルチセット) は、メンバーが複数回出現することを許可するコンテナーですが、そのメンバーには固有の順序がありません (リストとは異なります)。
次のシグネチャを持つ関数を探しています。
set of valid combinations =
generate_combinations(list of input bags, number of objects in valid combination)
有効な組み合わせのセットが Python で使用できるメモリを超える可能性があるためgenerate_combinations
、明示的なリストではなくジェネレータを返す必要があります。
有効な結果は、次の要件を満たす必要があります。
- 各入力バッグから少なくとも 1 つのオブジェクト
- 合計n個のオブジェクトがあります
私は次のことを想定しています:
- 結果内のオブジェクトの順序は重要ではありません
- 入力バッグには重複したオブジェクトを含めることができます
- 2 つのインプット バッグに重複するオブジェクトを含めることができます (縮退の場合、2 つのインプット バッグは同一にすることができます)。
- 有効な結果は、置換なしでオブジェクトをプルします
要件 1 と仮定 4 が意味することnumber of input bags <= n <= total number of objects in all bags
データ構造
- 入力バッグには重複する値を含めることが許可されているため (仮定 #2 に従って)、set/frozenset を使用してこれらを格納することはできません。Python リストは、このタスクに適しています。代わりのコンテナーはcollections.Counterである可能性があります。これは、一定時間のメンバーシップ テストと、多数の重複があるリストの空間効率の向上を備えています。
- 有効な各組み合わせもバッグです
- 入力バッグのリストでは順序は重要ではないため、これは入力バッグのバッグとして一般化できます。念のため、そのままにしておきます。
itertools
v2.6 で導入されたPython の組み込みモジュールを使用して組み合わせを生成します。古いバージョンの Python を実行している場合は、レシピを使用してください。これらのコード例では、わかりやすくするためにジェネレーターを暗黙的にリストに変換しました。
>>> import itertools, collections
>>> input_bags = [Bag([1,2,2,3,5]), Bag([1,4,5,9]), Bag([12])]
>>> output_bag_size = 5
>>> combos = generate_combinations(input_bags, output_bag_size)
>>> combos.next() #an arbitrary example
Bag([1,1,2,4,12])
上記の問題は、シーケンスの組み合わせを生成する Python の組み込み itertools モジュールによってすぐに解決できる問題に縮小できることを認識してください。変更する必要があるのは、要件 1 を削除することだけです。削減された問題を解決するには、バッグのメンバーを 1 つのバッグに結合します。
>>> all_objects = itertools.chain.from_iterable(input_bags)
>>> all_objects
generator that returns [1, 2, 2, 3, 5, 1, 4, 5, 9, 12]
>>> combos = itertools.combinations(all_objects, output_bag_size)
>>> combos
generator that returns [(1,2,2,3,5), (1,2,2,3,1), (1,2,2,3,4), ...]
要件 1 を元に戻すには、出力バッグのサイズがユーザー指定の値に達するまで、各有効な組み合わせ (出力バッグ) に各入力バッグの 1 つの要素と任意のバッグの追加要素を含める必要があります。[各入力バッグからの 1 要素] と [いずれかのバッグからの追加要素] の間のオーバーラップが無視される場合、解は最初の可能な組み合わせと 2 番目の可能な組み合わせのデカルト積になります。
オーバーラップを処理するには、[各入力バッグの 1 つの要素] の要素を [任意のバッグの追加要素] から削除し、for ループを取り除きます。
>>> combos_with_one_element_from_each_bag = itertools.product(*input_bags)
>>> for base_combo in combos_with_one_element_from_each_bag:
>>> all_objects = list(itertools.chain.from_iterable(input_bags))
>>> for seen in base_combo:
>>> all_objects.remove(seen)
>>> all_objects_minus_base_combo = all_objects
>>> for remaining_combo in itertools.combinations(all_objects_minus_base_combo, output_bag_size-len(base_combo)):
>>> yield itertools.chain(base_combo, remaining_combo)
削除操作はリストではサポートされていますが、ジェネレーターではサポートされていません。all_objects をリストとしてメモリに格納しないようにするには、base_combo の要素をスキップする関数を作成します。
>>> def remove_elements(iterable, elements_to_remove):
>>> elements_to_remove_copy = Bag(elements_to_remove) #create a soft copy
>>> for item in iterable:
>>> if item not in elements_to_remove_copy:
>>> yield item
>>> else:
>>> elements_to_remove_copy.remove(item)
Bag クラスの実装は次のようになります。
>>> class Bag(collections.Counter):
>>> def __iter__(self):
>>> return self.elements()
>>> def remove(self, item):
>>> self[item] -= 1
>>> if self[item] <= 0:
>>> del self[item]
完全なコード
import itertools, collections
class Bag(collections.Counter):
def __iter__(self):
return self.elements()
def remove(self, item):
self[item] -= 1
if self[item] <= 0:
del self[item]
def __repr__(self):
return 'Bag(%s)' % sorted(self.elements())
def remove_elements(iterable, elements_to_remove):
elements_to_remove_copy = Bag(elements_to_remove) #create a soft copy
for item in iterable:
if item not in elements_to_remove_copy:
yield item
else:
elements_to_remove_copy.remove(item)
def generate_combinations(input_bags, output_bag_size):
combos_with_one_element_from_each_bag = itertools.product(*input_bags)
for base_combo in combos_with_one_element_from_each_bag:
all_objects_minus_base_combo = remove_elements(itertools.chain.from_iterable(input_bags), base_combo)
for remaining_combo in itertools.combinations(all_objects_minus_base_combo, output_bag_size-len(base_combo)):
yield Bag(itertools.chain(base_combo, remaining_combo))
input_bags = [Bag([1,2,2,3,5]), Bag([1,4,5,9]), Bag([12])]
output_bag_size = 5
combos = generate_combinations(input_bags, output_bag_size)
list(combos)
エラー チェック コード (output_bag_size >= len(input_bags) など) を追加して、これを終了します。
このアプローチの利点は次のとおりです。 1. 維持するコードが少なくなります (つまり itertools) 2. 再帰がありません。Python のパフォーマンスはコール スタックの高さに応じて低下するため、再帰を最小限に抑えることが有益です。3.最小のメモリ消費。このアルゴリズムは、入力バッグのリストによって消費されるものを超えて一定スペースのメモリを必要とします。