Найти наиболее эффективные группы пар - PullRequest
0 голосов
/ 01 июля 2018

Задача

У меня есть группа людей, и я хочу, чтобы каждый человек имел встречу 1: 1 с каждым другим человеком в группе. Данный человек может встречаться только с одним человеком одновременно, поэтому я хочу сделать следующее:

  1. Найдите все возможные комбинации
  2. Сгруппируйте пары в «раунды» собраний, где каждый человек может участвовать в раунде только один раз, и в раунде должно быть как можно больше пар, чтобы удовлетворить все возможные комбинации пар в наименьшем количестве раундов.

Чтобы продемонстрировать проблему с точки зрения желаемого ввода / вывода, допустим, у меня есть следующий список:

>>> 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. Перебрать копию набора people_pairs, вычисленного с использованием метода combinations, описанного выше
  3. Для каждого человека в паре проверьте, существуют ли какие-либо существующие пары в текущем round, которые уже содержат этого человека
  4. Если уже есть пара, содержащая одного из индивидов, пропустите эту пару для этого раунда. Если нет, добавьте пару в раунд и удалите пару из списка 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

График эффективности этого метода для списков размером 100-300 с использованием https://mycurvefit.com показывает, что вычисление раундов для списка из 1000 человек, вероятно, займет около 100 минут. Есть ли более эффективный способ сделать это?

Примечание: я не на самом деле пытаюсь организовать встречу из 1000 человек :) Это всего лишь простой пример, который представляет проблему сопоставления / комбинаторики, которую я пытаюсь решить.

Ответы [ 6 ]

0 голосов
/ 01 июля 2018

Это реализация алгоритма, описанного в статье Википедии Турнир с круговым турниром .

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)]
    ]
0 голосов
/ 01 июля 2018

Я генерирую только индексы (потому что у меня не получается найти 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)
'''
0 голосов
/ 01 июля 2018

Две вещи, которые вы можете сделать сразу же:

  1. Не делайте копии набора каждый раз через список. Это большая трата времени / памяти. Вместо этого модифицируйте набор один раз после каждой итерации.

  2. Держите отдельный набор людей в каждом раунде. Поиск человека в наборе будет на порядок быстрее, чем просмотр всего раунда.

Ex:

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

0 голосов
/ 01 июля 2018

Это займет около 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. Это позволяет нам прекратить итерацию по всем комбинациям, когда пиши пусты, то есть, когда все в раунде.

0 голосов
/ 01 июля 2018

Может быть, я что-то упускаю (не совсем необычно), но это звучит как обычный старый турнир с круговым турниром, где каждая команда играет каждую команду ровно один раз.

Есть O (n ^ 2) методов для обработки этого "вручную", которые работают "на машине" просто отлично. Одно хорошее описание можно найти в статье в Википедии о турнирах Round-Robin .

Об этом O (n ^ 2): будет n-1 или n раундов, каждый из которых требует O (n) шагов, чтобы повернуть все, кроме одной записи таблицы, и O (n) шагов, чтобы перечислить совпадения n//2 в каждом раунде. Поворот O (1) можно выполнить с помощью двусвязных списков, но перечисление совпадений по-прежнему равно O (n). Итак, O (n) * O (n) = O (n ^ 2).

0 голосов
/ 01 июля 2018

Когда вам нужны быстрые поиски, лучше использовать хеши / диктанты. Следите за тем, кто был в каждом раунде в dict, а не list, и это будет намного быстрее.

Поскольку вы входите в алгоритмы, изучение больших O-обозначений поможет вам узнать, какие структуры данных хороши в том, какие операции также являются ключевыми. См. Это руководство: https://wiki.python.org/moin/TimeComplexity для временной сложности встроенных Python. Вы увидите, что проверка элемента в списке имеет значение O (n), что означает, что он масштабируется линейно с размером ваших входных данных. Так что, поскольку это в цикле, вы в конечном итоге с O (n ^ 2) или хуже. Для диктов, поиск обычно O (1), что означает, что размер ввода не имеет значения.

Кроме того, не переопределяйте встроенные модули. Я бы изменил round на round_

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_
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...