web-dev-qa-db-ja.com

重複するプールから順序付けられていない組み合わせを選択する

値のプールがあり、特定のプールから選択して、可能なすべての順序付けられていない組み合わせを生成したいと思います。

たとえば、プール0、プール0、およびプール1から選択したいとしました。

>>> pools = [[1, 2, 3], [2, 3, 4], [3, 4, 5]]
>>> part = (0, 0, 1)
>>> list(product(*(pools[i] for i in part)))
[(1, 1, 2), (1, 1, 3), (1, 1, 4), (1, 2, 2), (1, 2, 3), (1, 2, 4), (1, 3, 2), (1, 3, 3), (1, 3, 4), (2, 1, 2), (2, 1, 3), (2, 1, 4), (2, 2, 2), (2, 2, 3), (2, 2, 4), (2, 3, 2), (2, 3, 3), (2, 3, 4), (3, 1, 2), (3, 1, 3), (3, 1, 4), (3, 2, 2), (3, 2, 3), (3, 2, 4), (3, 3, 2), (3, 3, 3), (3, 3, 4)]

これにより、プール0、プール0、およびプール1から選択することにより、可能なすべての組み合わせが生成されます。

ただし、順序は私には関係ないので、組み合わせの多くは実際には重複しています。たとえば、デカルト積を使用したため、(1, 2, 4)(2, 1, 4)の両方が生成されます。

この問題を軽減する簡単な方法を思いつきました。単一のプールから選択されたメンバーの場合、combinations_with_replacementを使用して注文せずに選択します。各プールから何回引き出したいかを数えます。コードは次のようになります。

cnt = Counter()
for ind in part: cnt[ind] += 1
blocks = [combinations_with_replacement(pools[i], cnt[i]) for i in cnt]
return [list(chain(*combo)) for combo in product(*blocks)]

これにより、同じプールから複数回選択した場合に重複する順序が減ります。ただし、すべてのプールには多くの重複があり、マージされた複数のプールでcombinations_with_replacementを使用すると、いくつかの無効な組み合わせが生成されます。順序付けられていない組み合わせを生成するためのより効率的な方法はありますか?

編集:入力に関する追加情報:パーツとプールの数は少なく(〜5と〜20)、簡単にするために、各要素は整数です。私がすでに解決した実際の問題なので、これは学術的な興味のためだけです。あるとしましょう 数千人 各プールには数百の整数がありますが、一部のプールは小さく、数十しかありません。したがって、ある種の結合または交差点が進むべき道のようです。

25
qwr

これは難しい問題です。一般的な場合の最善の策は、キーがmultisetで、値が実際の組み合わせであるhash tableを実装することだと思います。これは@ErikWolfが述べたものと似ていますが、このメソッドは最初から重複を生成することを回避するため、フィルタリングは必要ありません。また、multisetsに遭遇したときに正しい結果を返します。

私が今からかっているより速い解決策がありますが、後で保存します。我慢して。

コメントで述べたように、実行可能と思われる1つのアプローチは、すべてのプールを組み合わせて、この組み合わせたプールの組み合わせを生成し、プールの数を選択することです。マルチセットの組み合わせを生成できるツールが必要になります。これは、pythonで利用できることがわかっています。 sympyライブラリfrom sympy.utilities.iterables import multiset_combinationsにあります。これに伴う問題は、依然として重複する値を生成し、さらに悪いことに、類似のsetproductの組み合わせでは取得できない結果を生成することです。たとえば、OPのすべてのプールを並べ替えて結合し、次のように適用する場合です。

list(multiset_permutations([1,2,2,3,3,4,4,5]))

結果のいくつかは[1 2 2][4 4 5]であり、どちらも[[1, 2, 3], [2, 3, 4], [3, 4, 5]]から取得することは不可能です。

特別な場合を除いて、考えられるすべての製品をチェックすることを回避する方法がわかりません。私が間違っているといいのですが。

アルゴリズムの概要
主なアイデアは、重複を除外することなく、ベクトルの積の組み合わせを一意の組み合わせにマッピングすることです。 OPによって与えられた例(つまり、(1, 2, 3)(1, 3, 2))は、1つの値(順序は関係ないので、どちらか一方)にのみマップする必要があります。 2つのベクトルが同一のセットであることに注意してください。今、私たちは次のような状況もあります:

vec1 = (1, 2, 1)
vec2 = (2, 1, 1)
vec3 = (2, 2, 1)

同じ値にマップするにはvec1vec2が必要ですが、vec3は独自の値にマップする必要があります。これらはすべて同等であるため、これはセットの問題です sets (セットの場合、要素は一意であるため、{a, b, b}{a, b}は同等です)。

ここで multisets が役立ちます。マルチセットでは、(2, 2, 1)(1, 2, 1)は区別されますが、(1, 2, 1)(2, 1, 1)は同じです。これはいい。これで、一意のキーを生成するメソッドができました。

私はpythonプログラマーではないので、C++に進みます。

上記のすべてをそのまま実装しようとすると、いくつかの問題が発生します。私の知る限り、std::multiset<int>のキー部分としてstd::unordered_mapを使用することはできません。ただし、通常のstd::mapは可能です。下のハッシュテーブルほどパフォーマンスは高くありませんが(実際には 赤黒木 )、それでもまともなパフォーマンスが得られます。ここにあります:

void cartestionCombos(std::vector<std::vector<int> > v, bool verbose) {

    std::map<std::multiset<int>, std::vector<int> > cartCombs;

    unsigned long int len = v.size();
    unsigned long int myProd = 1;
    std::vector<unsigned long int> s(len);

    for (std::size_t j = 0; j < len; ++j) {
        myProd *= v[j].size();
        s[j] = v[j].size() - 1;
    }

    unsigned long int loopLim = myProd - 1;
    std::vector<std::vector<int> > res(myProd, std::vector<int>());
    std::vector<unsigned long int> myCounter(len, 0);
    std::vector<int> value(len, 0);
    std::multiset<int> key;

    for (std::size_t j = 0; j < loopLim; ++j) {
        key.clear();

        for (std::size_t k = 0; k < len; ++k) {
            value[k] = v[k][myCounter[k]];
            key.insert(value[k]);
        }

        cartCombs.insert({key, value});

        int test = 0;
        while (myCounter[test] == s[test]) {
            myCounter[test] = 0;
            ++test;
        }

        ++myCounter[test];
    }

    key.clear();
    // Get last possible combination
    for (std::size_t k = 0; k < len; ++k) {
        value[k] = v[k][myCounter[k]];
        key.insert(value[k]);
    }

    cartCombs.insert({key, value});

    if (verbose) {
        int count = 1;

        for (std::pair<std::multiset<int>, std::vector<int> > element : cartCombs) {
            std::string tempStr;

            for (std::size_t k = 0; k < len; ++k)
                tempStr += std::to_string(element.second[k]) + ' ';

            std::cout << count << " : " << tempStr << std::endl;
            ++count;
        }
    }
}

1から15までのランダムな整数で満たされた4から8までの長さの8つのベクトルのテストケースでは、上記のアルゴリズムは私のコンピューターで約5秒で実行されます。私たちの製品から合計250万近くの結果を見ていることを考えると、それは悪いことではありませんが、もっとうまくいくことができます。しかし、どのように?

最高のパフォーマンスは、一定時間で構築されたキーを使用したstd::unordered_mapによって提供されます。上記のキーは対数時間に組み込まれています( マルチセット、マップ、およびハッシュマップの複雑さ )。だから問題は、どうすればこれらのハードルを克服できるかということです。

最高のパフォーマンス

std::multisetを放棄しなければならないことはわかっています。 commutative typeプロパティを持ちながら、一意の結果を提供する、ある種のオブジェクトが必要です。

入力 算術の基本定理

それは、すべての数が素数の積によって(因子の次数まで)一意に表すことができると述べています。これは素因数分解と呼ばれることもあります。

これで、以前と同じように簡単に進めることができますが、マルチセットを作成する代わりに、各インデックスを素数にマップして結果を乗算します。これにより、キーの一定時間の構築が可能になります。これは、上記で作成した例に対するこの手法の威力を示す例です(N.B. P以下は素数のリストです...(2, 3, 5, 7, 11, etc.)

                   Maps to                    Maps to            product
vec1 = (1, 2, 1)    -->>    P[1], P[2], P[1]   --->>   3, 5, 3    -->>    45
vec2 = (2, 1, 1)    -->>    P[2], P[1], P[1]   --->>   5, 3, 3    -->>    45
vec3 = (2, 2, 1)    -->>    P[2], P[2], P[1]   --->>   5, 5, 3    -->>    75

これはすごい! vec1vec2は同じ番号にマップされますが、vec3は希望どおりに異なる値にマップされます。

void cartestionCombosPrimes(std::vector<std::vector<int> > v, 
                        std::vector<int> primes,
                        bool verbose) {

    std::unordered_map<int64_t, std::vector<int> > cartCombs;

    unsigned long int len = v.size();
    unsigned long int myProd = 1;
    std::vector<unsigned long int> s(len);

    for (std::size_t j = 0; j < len; ++j) {
        myProd *= v[j].size();
        s[j] = v[j].size() - 1;
    }

    unsigned long int loopLim = myProd - 1;
    std::vector<std::vector<int> > res(myProd, std::vector<int>());
    std::vector<unsigned long int> myCounter(len, 0);
    std::vector<int> value(len, 0);
    int64_t key;

    for (std::size_t j = 0; j < loopLim; ++j) {
        key = 1;

        for (std::size_t k = 0; k < len; ++k) {
            value[k] = v[k][myCounter[k]];
            key *= primes[value[k]];
        }

        cartCombs.insert({key, value});

        int test = 0;
        while (myCounter[test] == s[test]) {
            myCounter[test] = 0;
            ++test;
        }

        ++myCounter[test];
    }

    key = 1;
    // Get last possible combination
    for (std::size_t k = 0; k < len; ++k) {
        value[k] = v[k][myCounter[k]];
        key *= primes[value[k]];
    }

    cartCombs.insert({key, value});
    std::cout << cartCombs.size() << std::endl;

    if (verbose) {
        int count = 1;

        for (std::pair<int, std::vector<int> > element : cartCombs) {
            std::string tempStr;

            for (std::size_t k = 0; k < len; ++k)
                tempStr += std::to_string(element.second[k]) + ' ';

            std::cout << count << " : " << tempStr << std::endl;
            ++count;
        }
    }
}

ほぼ250万の製品を生成する上記の同じ例では、上記のアルゴリズムは0.3秒未満で同じ結果を返します。

この後者の方法にはいくつかの注意点があります。素数を事前に生成する必要があります。デカルト積に多くのベクトルがある場合、キーはint64_tの範囲を超えて大きくなる可能性があります。素数を生成するために利用できる多くのリソース(ライブラリ、ルックアップテーブルなど)があるため、最初の問題を克服するのはそれほど難しいことではありません。よくわかりませんが、整数の精度は任意であるため、後者の問題はpythonでは問題にならないはずです( Python整数範囲 )。

また、ソースベクトルが小さな値のNice整数ベクトルではない可能性があるという事実にも対処する必要があります。これは、先に進む前に、すべてのベクトルにわたってすべての要素をランク付けすることで解決できます。たとえば、次のベクトルがあるとします。

vec1 = (12345.65, 5, 5432.11111)
vec2 = (2222.22, 0.000005, 5)
vec3 = (5, 0.5, 0.8)

それらをランク付けすると、次のようになります。

rank1 = (6, 3, 5)
rank2 = (4, 0, 3)
rank3 = (3, 1, 2)

そして今、これらを実際の値の代わりに使用してキーを作成できます。変更されるコードの唯一の部分は、キーを構築するforループ(そしてもちろん、作成する必要があるrankオブジェクト)です。

for (std::size_t k = 0; k < len; ++k) {
    value[k] = v[k][myCounter[k]];
    key *= primes[rank[k][myCounter[k]]];
}

編集:
一部のコメント提供者が指摘しているように、上記の方法は、すべての製品を生成する必要があるという事実を偽装しています。私はそれを初めて言ったはずです。個人的には、さまざまなプレゼンテーションを考えると、どうすれば回避できるのかわかりません。

また、誰かが興味を持っている場合に備えて、上記で使用したテストケースを次に示します。

[1 10 14  6],
[7  2  4  8  3 11 12],
[11  3 13  4 15  8  6  5],
[10  1  3  2  9  5  7],
[1  5 10  3  8 14],
[15  3  7 10  4  5  8  6],
[14  9 11 15],
[7  6 13 14 10 11  9  4]

162295個の一意の組み合わせを返す必要があります。

8
Joseph Wood

一部の作業を節約する1つの方法は、最初に選択したk個のプールの重複排除された組み合わせを生成し、それらを最初のk +1の重複排除された組み合わせに拡張することです。これにより、最初の2つのプールから2, 1ではなく1, 2を選択したすべての長さ20の組み合わせを個別に生成して拒否することを回避できます。

def combinations_from_pools(pools):
    # 1-element set whose one element is an empty Tuple.
    # With no built-in hashable multiset type, sorted tuples are probably the most efficient
    # multiset representation.
    combos = {()}
    for pool in pools:
        combos = {Tuple(sorted(combo + (elem,))) for combo in combos for elem in pool}
    return combos

ただし、話している入力サイズでは、組み合わせをどれだけ効率的に生成しても、すべてを処理することはできません。 20個の同一の1000要素プールがある場合でも、496432432489450355564471512635900731810050の組み合わせ(1019は星と棒の式で20を選択)、つまり約5e41になります。あなたが地球を征服し、すべての人類のすべての計算装置のすべての処理能力をその仕事に捧げたとしても、あなたはまだその中にへこみを作ることができませんでした。根本的なタスクを解決するためのより良い方法を見つける必要があります。

これまでに投稿された回答( 怠惰な辞書式順序の生成 Tim Petersによる)は、出力のサイズに比例する最悪の場合のスペースの複雑さを持っています。内部で生成された中間データを重複排除することなく、すべての一意の順序付けられていない組み合わせを建設的に生成するアプローチの概要を説明します。私のアルゴリズムは、辞書式順序で組み合わせを生成します。単純なアルゴリズムと比較して、計算のオーバーヘッドがあります。ただし、並列化することはできます(そのため、最終出力のさまざまな範囲を同時に生成できます)。

アイデアは次のとおりです。

したがって、Nプール{P1、...、PN}ここから組み合わせを描画する必要があります。最小の組み合わせを簡単に識別できます(前述の辞書式順序に関して)。 (x1、 バツ2 ...、 バツN-1、 バツN)(ここでx1 <= x2 <= ... <= xN-1 <= xN、および各xj プールの1つからの最小要素{P})。この最小の組み合わせの後に、接頭辞xが付いた0個以上の組み合わせが続きます。1、 バツ2 ...、 バツN-1 は同じであり、最後の位置は値の増加するシーケンス上を実行します。そのシーケンスをどのように識別できますか?

次の定義を紹介しましょう。

与えられた組み合わせプレフィックスC =(x1、 バツ2 ...、 バツK-1、 バツK)(K <Nの場合)、プールP 後者(プレフィックス)を残りのプールから引き出すことができる場合は、Cに関しては無料と呼ばれます。

特定のプレフィックスの空きプールを特定することは、2部グラフで最大値を見つけるという問題に簡単に還元されます マッチング 。難しい部分は、それを効率的に行うことです(私たちのケースの詳細を利用します)。しかし、後で使用するために保存します(これは進行中の作業であり、Python 1日でプログラムとして実現されます)。

したがって、接頭辞(x1、 バツ2 ...、 バツN-1)最初の組み合わせで、すべての空きプールを識別できます{FP}。それらのいずれかを使用して、最後の位置の要素を選択できます。したがって、対象のシーケンスは、{FPからのソートされた要素のセットです。1 U FP2 x以上のU ...}N-1

最後の位置が使い果たされたら、最後の1つだけの位置を増やす必要があります。その後、最後の位置の可能な値を見つける手順を繰り返します。当然のことながら、最後から1つ(およびその他の)位置の値を列挙する手順は同じです。唯一の違いは、空きプールを識別する必要がある組み合わせプレフィックスの長さです。

したがって、次の再帰的アルゴリズムが機能するはずです。

  1. 空の組み合わせプレフィックスCから始めます。この時点で、すべてのプールは空いています。
  2. Cの長さがNに等しい場合は、Cを出力して戻ります。
  3. 空きプールを1つのソートされたリストSにマージし、Cの最後の要素よりも小さいすべての要素をそこから削除します。
  4. Sからの各値xについて
    • 新しい組み合わせプレフィックスはC '=(C、x)です。
    • 現在の組み合わせプレフィックスが1つ増えると、一部の空きプールは空きがなくなります。それらを識別し、更新された空きプールリストと組み合わせプレフィックスC 'を使用してステップ1に戻ります。
5
Leon

ハッシュ可能なリストを実装し、python set()を使用してすべての重複をフィルタリングできます。ハッシュ関数は、collections.Counterを使用して実現できるリスト内の順序を無視する必要があります。

from collections import Counter

class HashableList(list):
    def __hash__(self):
        return hash(frozenset(Counter(self)))
    def __eq__(self, other):
        return hash(self) == hash(other)

x = HashableList([1,2,3])
y = HashableList([3,2,1])

print set([x,y])

これは次を返します:

set([[1, 2, 3]])
3
Erik Wolf

これは私が思いついたものです:

class Combination:
    def __init__(self, combination):
        self.combination = Tuple(sorted(combination))

    def __eq__(self, other):
        return self.combination == self.combination

    def __hash__(self):
        return self.combination.__hash__()

    def __repr__(self):
        return self.combination.__repr__()

    def __getitem__(self, i):
        return self.combination[i]

次に、

pools = [[1, 2, 3], [2, 3, 4], [3, 4, 5]]
part = (0, 0, 1)
set(Combination(combin) for combin in product(*(pools[i] for i in part)))

出力:

{(1, 1, 2),
 (1, 1, 3),
 (1, 1, 4),
 (1, 2, 2),
 (1, 2, 3),
 (1, 2, 4),
 (1, 3, 3),
 (1, 3, 4),
 (2, 2, 2),
 (2, 2, 3),
 (2, 2, 4),
 (2, 3, 3),
 (2, 3, 4),
 (3, 3, 3),
 (3, 3, 4)}

これが本当にあなたが探しているものであるかどうかはわかりません。

2
PMende