web-dev-qa-db-ja.com

ペアの最も効率的なグループを見つける

問題

私にはグループがあり、各人がグループ内の他のすべての人と1対1でミーティングを行うことを望んでいます。ある人は一度に一人しか会えないので、次のことをしたいと思います。

  1. 可能なすべてのペアリングの組み合わせを見つける
  2. ペアを会議の「ラウンド」にグループ化します。各人は1回だけラウンドに参加でき、ラウンドには、最小数のラウンドで可能なすべてのペアの組み合わせを満たすために、できるだけ多くのペアを含める必要があります。

必要な入力/出力の観点から問題を示すために、次のリストがあるとします。

>>> people = ['Dave', 'Mary', 'Susan', 'John']

次の出力を生成したいと思います。

>>> for round in make_rounds(people):
>>>     print(round)
[('Dave', 'Mary'), ('Susan', 'John')]
[('Dave', 'Susan'), ('Mary', 'John')]
[('Dave', 'John'), ('Mary', 'Susan')]

奇数の人がいた場合、次の結果が期待できます。

>>> people = ['Dave', 'Mary', 'Susan']
>>> for round in make_rounds(people):
>>>     print(round)
[('Dave', 'Mary')]
[('Dave', 'Susan')]
[('Mary', 'Susan')]

この問題の鍵は、(理由の範囲内で)パフォーマンスを発揮するソリューションが必要なことです。 が機能するコードを作成しましたが、peopleのサイズが大きくなると、指数関数的に遅くなります。コードが非効率的であるかどうか、または単に問題のパラメーターに拘束されているかどうかを知るために、パフォーマンスの高いアルゴリズムを作成することについて十分に知りません。

私が試したこと

ステップ1は簡単です:itertools.combinationsを使用してすべての可能なペアリングを取得できます。

>>> from itertools import combinations
>>> people_pairs = set(combinations(people, 2))
>>> print(people_pairs)
{('Dave', 'Mary'), ('Dave', 'Susan'), ('Dave', 'John'), ('Mary', 'Susan'), ('Mary', 'John'), ('Susan', 'John')}

ラウンド自体を解決するために、私は次のようなラウンドを構築しています。

  1. 空のroundリストを作成します
  2. 上記のcombinationsメソッドを使用して計算されたpeople_pairsセットのコピーを反復処理します
  3. ペアの各人について、現在のround内にその個人がすでに含まれている既存のペアがあるかどうかを確認します
  4. 個人の1人を含むペアがすでに存在する場合は、このラウンドではそのペアリングをスキップします。そうでない場合は、ペアをラウンドに追加し、people_pairsリストからペアを削除します。
  5. すべての人のペアが繰り返されたら、そのラウンドをマスターroundsリストに追加します
  6. people_pairsには、最初のラウンドに参加しなかったペアのみが含まれるようになったため、もう一度やり直してください

最終的に、これは望ましい結果を生み出し、残りがなくなり、すべてのラウンドが計算されるまで、私の人々のペアを削り落とします。これにはばかげた回数の反復が必要であることがすでにわかりますが、これを行うためのより良い方法はわかりません。

これが私のコードです:

from itertools import combinations

# test if person already exists in any pairing inside a round of pairs
def person_in_round(person, round):
    is_in_round = any(person in pair for pair in round)
    return is_in_round

def make_rounds(people):
    people_pairs = set(combinations(people, 2))
    # we will remove pairings from people_pairs whilst we build rounds, so loop as long as people_pairs is not empty
    while people_pairs:
        round = []
        # make a copy of the current state of people_pairs to iterate over safely
        for pair in set(people_pairs):
            if not person_in_round(pair[0], round) and not person_in_round(pair[1], round):
                round.append(pair)
                people_pairs.remove(pair)
        yield round

https://mycurvefit.com を使用してリストサイズが100〜300の場合にこのメソッドのパフォーマンスをプロットすると、1000人のリストのラウンドの計算にはおそらく約100分かかることがわかります。これを行うためのより効率的な方法はありますか?

注:私は実際には1000人の会議を開催しようとはしていません:)これはマッチング/組み合わせ論の問題を表す単純な例です ' m解決しようとしています。

24

これは、ウィキペディアの記事 ラウンドロビントーナメント で説明されているアルゴリズムの実装です。

from itertools import cycle , islice, chain

def round_robin(iterable):
    items = list(iterable)
    if len(items) % 2 != 0:
        items.append(None)
    fixed = items[:1]
    cyclers = cycle(items[1:])
    rounds = len(items) - 1
    npairs = len(items) // 2
    return [
        list(Zip(
            chain(fixed, islice(cyclers, npairs-1)),
            reversed(list(islice(cyclers, npairs)))
        ))
        for _ in range(rounds)
        for _ in [next(cyclers)]
    ]
15

インデックスだけを生成しますが(1000の名前=を思い付くのに問題があるため)、1000の数値の場合、実行時間は約4秒です。

他のすべてのアプローチの主な問題-それらはペアを使用してそれらと連携し、ペアがたくさんあり、実行時間がはるかに長くなっています。私のアプローチは、ペアではなく、人との作業で異なります。私はその人を彼が会わなければならない他の人のリストにマップするdict()を持っています、そしてこれらのリストは最大でNアイテムの長さです(ペアのようにN ^ 2ではありません)。したがって、時間の節約になります。

#!/usr/bin/env python

from itertools import combinations
from collections import defaultdict

pairs = combinations( range(6), 2 )

pdict = defaultdict(list)
for p in pairs :
    pdict[p[0]].append( p[1] )

while len(pdict) :
    busy = set()
    print '-----'
    for p0 in pdict :
        if p0 in busy : continue

        for p1 in pdict[p0] :
            if p1 in busy : continue

            pdict[p0].remove( p1 )
            busy.add(p0)
            busy.add(p1)
            print (p0, p1)

            break

    # remove empty entries
    pdict = { k : v for k,v in pdict.items() if len(v) > 0 }

'''
output:
-----
(0, 1)
(2, 3)
(4, 5)
-----
(0, 2)
(1, 3)
-----
(0, 3)
(1, 2)
-----
(0, 4)
(1, 5)
-----
(0, 5)
(1, 4)
-----
(2, 4)
(3, 5)
-----
(2, 5)
(3, 4)
'''
6
lenik

すぐにできる2つのこと:

  1. リストから毎回セットのコピーを作成しないでください。それは時間/メモリの大きな無駄です。代わりに、各反復後にセットを1回変更します。

  2. 各ラウンドで別々の人々のセットを維持します。セット内の人物を検索すると、ラウンド全体をループするよりも1桁速くなります。

例:

def make_rounds(people):
    people_pairs = set(combinations(people, 2))

    while people_pairs:
        round = set()
        people_covered = set()
        for pair in people_pairs:
            if pair[0] not in people_covered \
               and pair[1] not in people_covered:
                round.add(pair)
                people_covered.update(pair)
        people_pairs -= round # remove thi
        yield round

比較: time comparison

4
soundstripe

高速ルックアップが必要な場合は、ハッシュ/ディクテーションが最適です。 dictではなくlistで各ラウンドに誰が参加したかを追跡すると、はるかに高速になります。

アルゴリズムを取り入れているので、大きなO表記を研究することは、どのデータ構造がどのような操作に適しているかを知るのに役立ちます。このガイドを参照してください: https://wiki.python.org/moin/TimeComplexity 時間計算量Pythonビルトイン)については、リスト内の項目はO(n)であり、入力のサイズに比例してスケーリングすることを意味します。したがって、ループ内にあるため、最終的にO(n ^ 2)以下になります。 dictの場合、ルックアップは通常O(1)です。つまり、入力のサイズは関係ありません。

また、組み込みをオーバーライドしないでください。 roundround_に変更しました

from itertools import combinations

# test if person already exists in any pairing inside a round of pairs
def person_in_round(person, people_dict):
    return people_dict.get(person, False)

def make_rounds(people):
    people_pairs = set(combinations(people, 2))
    people_in_round = {}
    # we will remove pairings from people_pairs whilst we build rounds, so loop as long as people_pairs is not empty
    while people_pairs:
        round_ = []
        people_dict = {}
        # make a copy of the current state of people_pairs to iterate over safely
        for pair in set(people_pairs):
            if not person_in_round(pair[0], people_dict) and not person_in_round(pair[1], people_dict):
                round_.append(pair)
                people_dict[pair[0]] = True
                people_dict[pair[1]] = True


                people_pairs.remove(pair)
        yield round_
3
Zev

何かが足りないかもしれませんが(まったく珍しいことではありません)、これは、各チームが他のすべてのチームを1回だけプレイする昔ながらのラウンドロビントーナメントのように聞こえます。

これを「手作業で」処理するO(n ^ 2)メソッドがあり、「機械で」問題なく機能します。 1つの良い説明が見つかります ラウンドロビントーナメントに関するウィキペディアの記事に

そのO(n ^ 2)について:n-1またはnラウンドのいずれかがあり、それぞれが1つを除くすべてのテーブルエントリをローテーションするためのO(n) 1ステップとO(n)各ラウンドでn//2一致を列挙する手順。二重リンクリストを使用してローテーションO(1)を作成できますが、一致はまだO(n)です。したがって、O(n)* O(n)= O(n ^ 2)です。

2
Mike Housky

これは私のコンピューターで約45秒かかります

def make_rnds(people):
    people_pairs = set(combinations(people, 2))
    # we will remove pairings from people_pairs whilst we build rnds, so loop as long as people_pairs is not empty
    while people_pairs:
        rnd = []
        rnd_set = set()
        peeps = set(people)
        # make a copy of the current state of people_pairs to iterate over safely
        for pair in set(people_pairs):
            if pair[0] not in rnd_set and pair[1] not in rnd_set:
                rnd_set.update(pair)
                rnd.append(pair)

                peeps.remove(pair[0])
                peeps.remove(pair[1])

                people_pairs.remove(pair)
                if not peeps:
                    break
        yield rnd

関数を削除しましたperson_in_rnd関数呼び出しで失われる時間を削減し、rnd_setおよびpeepsという変数を追加しました。 rnd_setは、これまでのラウンドの全員のセットであり、ペアとの一致をチェックするために使用されます。 peepsはコピーされた人々のセットであり、rndにペアを追加するたびに、それらの個人をpeepsから削除します。これにより、ピープが空になると、つまり全員がラウンドに入ると、すべての組み合わせの反復を停止できます。

1
Sam Kowalski