Секретный алгоритм Санты - PullRequest
24 голосов
/ 07 ноября 2008

Каждое Рождество мы рисуем имена для обмена подарками в моей семье. Это обычно включает в себя несколько перерисовок, пока никто не вытащил их супруга. Поэтому в этом году я написал свое собственное приложение для рисования имен, которое принимает кучу имен, кучу запрещенных пар и отправляет электронное письмо всем, у кого есть выбранный подарок.

В данный момент алгоритм работает следующим образом (в псевдокоде):

function DrawNames(list allPeople, map disallowedPairs) returns map
    // Make a list of potential candidates
    foreach person in allPeople
        person.potentialGiftees = People
        person.potentialGiftees.Remove(person)
        foreach pair in disallowedPairs
            if pair.first = person
               person.Remove(pair.second)

    // Loop through everyone and draw names
    while allPeople.count > 0
        currentPerson = allPeople.findPersonWithLeastPotentialGiftees
        giftee = pickRandomPersonFrom(currentPerson.potentialGiftees)
        matches[currentPerson] = giftee
        allPeople.Remove(currentPerson)
        foreach person in allPeople
            person.RemoveIfExists(giftee)

    return matches

Кто-нибудь, кто знает больше о теории графов, знает какой-нибудь алгоритм, который бы работал лучше здесь? Для моих целей это работает, но мне любопытно.

РЕДАКТИРОВАТЬ: Так как электронные письма вышли некоторое время назад, и я просто надеюсь узнать что-то, я перефразирую это как вопрос теории графов. Меня не очень интересуют особые случаи, когда исключениями являются все пары (например, супруги не получают друг друга). Меня больше интересуют случаи, когда существует достаточно исключений, когда поиск любого решения становится трудной частью. Мой алгоритм выше - это простой жадный алгоритм, который, я не уверен, будет успешным во всех случаях.

Начиная с полного ориентированного графа и списка пар вершин. Для каждой пары вершин удалите ребро от первой вершины до второй.

Цель состоит в том, чтобы получить график, в котором каждая вершина имеет одно входящее ребро и одно уходящее ребро.

Ответы [ 9 ]

9 голосов
/ 08 ноября 2008

Просто создайте график с ребрами, соединяющими двух человек, если им разрешено делиться подарками, а затем используйте алгоритм идеального соответствия. (Ищите «Пути, деревья и цветы» для (умного) алгоритма)

6 голосов
/ 20 ноября 2008

Я просто делал это сам, в конце концов, алгоритм, который я использовал, точно не моделирует рисование имен из шляпы, но это чертовски близко. В основном перемешайте список, а затем соедините каждого человека со следующим человеком в списке. Единственная разница с рисованием имен из шляпы состоит в том, что вы получаете один цикл вместо того, чтобы потенциально получить мини-подгруппы людей, которые только обмениваются подарками друг с другом. Во всяком случае, это может быть функция.

Реализация на Python:

import random
from collections import deque
def pairup(people):
    """ Given a list of people, assign each one a secret santa partner
    from the list and return the pairings as a dict. Implemented to always
    create a perfect cycle"""
    random.shuffle(people)
    partners = deque(people)
    partners.rotate()
    return dict(zip(people,partners))
6 голосов
/ 07 ноября 2008

Я бы не использовал запрещенные пары, поскольку это значительно усложняет задачу. Просто введите имя и адрес каждого в список. Создайте копию списка и продолжайте перетасовывать ее, пока адреса в каждой позиции двух списков не совпадут. Это гарантирует, что никто не получит себя или своего супруга.

В качестве бонуса, если вы хотите использовать этот стиль тайного голосования, печатайте конверты из первого списка и имена из второго списка. Не заглядывайте, когда набиваете конверты. (Или вы можете просто автоматизировать рассылку по электронной почте всем, кто их выбирает.)

Существует еще больше решений этой проблемы в этой теме .

5 голосов
/ 07 ноября 2008

Хммм. Я прошел курс теории графов, но проще было просто случайным образом переставить ваш список, соединить каждую группу подряд, а затем поменять любой элемент, который запрещен, другим. Поскольку в любой заданной паре нет запрещенных лиц, своп всегда будет успешным, если вы не разрешите свопы с выбранной группой. Ваш алгоритм слишком сложен.

2 голосов
/ 31 декабря 2011

В теории графов существует понятие, называемое гамильтонова цепь , которое описывает «цель», которую вы описываете. Один совет для любого, кто найдет это, - сообщить пользователям, какое «семя» использовалось для создания графика. Таким образом, если вам нужно заново сгенерировать график, вы можете. «Семя» также полезно, если вам нужно добавить или удалить человека. В этом случае просто выберите новое «начальное число» и сгенерируйте новый график, сообщив участникам, какое «начальное число» является текущим / последним.

2 голосов
/ 10 ноября 2010

Создайте график, в котором каждое ребро является «подарочной». Вершины, представляющие супругов, НЕ будут смежными. Выберите ребро наугад (это подарок). Удалите все ребра, идущие от подарка, и все ребра, идущие к приемнику, и повторите.

1 голос
/ 11 октября 2012

Вот простая реализация в Java для секретной проблемы Санты.

public static void main(String[] args) {
    ArrayList<String> donor = new ArrayList<String>();
    donor.add("Micha");
    donor.add("Christoph");
    donor.add("Benj");
    donor.add("Andi");
    donor.add("Test");
    ArrayList<String> receiver = (ArrayList<String>) donor.clone();

    Collections.shuffle(donor);
    for (int i = 0; i < donor.size(); i++) {
        Collections.shuffle(receiver);
        int target = 0;
        if(receiver.get(target).equals(donor.get(i))){              
            target++;
        }           
        System.out.println(donor.get(i) + " => " + receiver.get(target));
        receiver.remove(receiver.get(target));
    }
}
1 голос
/ 28 ноября 2008

Я только что создал веб-приложение, которое будет делать именно это - http://www.secretsantaswap.com/

Мой алгоритм допускает подгруппы. Это не красиво, но работает.

Работает следующим образом:
1. назначьте уникальный идентификатор всем участникам, запомните, в какой подгруппе они находятся
2. дублировать и перемешать этот список (цели)
3. создать массив количества участников в каждой подгруппе
4. дублированный массив из [3] для целей
5. создать новый массив для хранения финальных матчей
6. переберите участников, назначающих первую цель, которая не соответствует ни одному из следующих критериев:
A. участник == цель
B. участник. Подгруппа == цель. Подгруппа.
C. Выбор цели приведет к сбою подгруппы в будущем (например, в подгруппе 1 всегда должно быть как минимум столько же целей, не относящихся к подгруппе 1, сколько осталось участников подгруппы 1)
D. участник (n + 1) == цель (n +1)
Если мы назначаем цель, мы также уменьшаем массивы с 3 до 4

.

Так, не очень (вообще), но это работает. Надеюсь, это поможет,

Дэн Карлсон

0 голосов
/ 24 ноября 2017

Python решение здесь.

Учитывая последовательность (person, tags), где теги сами по себе являются (возможно, пустой) последовательностью строк, мой алгоритм предлагает цепочку людей, где каждый человек дарит подарок следующему в цепочке (последний человек явно в паре с первым).

Теги существуют для того, чтобы лица могли быть сгруппированы, и каждый раз, когда следующий человек выбирается из группы, наиболее разобщенной с последним выбранным человеком. Первоначальный человек выбирается пустым набором тегов, поэтому он будет выбран из самой длинной группы.

Итак, при заданной последовательности ввода:

example_sequence= [
    ("person1", ("male", "company1")),
    ("person2", ("female", "company2")),
    ("person3", ("male", "company1")),
    ("husband1", ("male", "company2", "marriage1")),
    ("wife1", ("female", "company1", "marriage1")),
    ("husband2", ("male", "company3", "marriage2")),
    ("wife2", ("female", "company2", "marriage2")),
]

предложение:

['person1 [мужчина, компания1]', 'person2 [женщина, компания2]', 'person3 [мужчина, компания1]', 'жена2 [женщина, брак2, компания2]', 'Муж1 [мужчина, брак1, компания2]', 'Муж2 [мужчина, брак2, компания3]', 'жена1 [женщина, брак1, компания1]']

Конечно, если у всех людей нет тегов (например, пустой кортеж), тогда есть только одна группа на выбор.

Не всегда существует оптимальное решение (представьте, что входная последовательность состоит из 10 женщин и 2 мужчин, причем единственным указанным тегом является их жанр), но это делает хорошую работу настолько, насколько это возможно.

Py2 / 3 совместимый.

import random, collections

class Statistics(object):
    def __init__(self):
        self.tags = collections.defaultdict(int)

    def account(self, tags):
        for tag in tags:
            self.tags[tag] += 1

    def tags_value(self, tags):
        return sum(1./self.tags[tag] for tag in tags)

    def most_disjoined(self, tags, groups):
        return max(
            groups.items(),
            key=lambda kv: (
                -self.tags_value(kv[0] & tags),
                len(kv[1]),
                self.tags_value(tags - kv[0]) - self.tags_value(kv[0] - tags),
            )
        )

def secret_santa(people_and_their_tags):
    """Secret santa algorithm.

    The lottery function expects a sequence of:
    (name, tags)

    For example:

    [
        ("person1", ("male", "company1")),
        ("person2", ("female", "company2")),
        ("person3", ("male", "company1")),
        ("husband1", ("male", "company2", "marriage1")),
        ("wife1", ("female", "company1", "marriage1")),
        ("husband2", ("male", "company3", "marriage2")),
        ("wife2", ("female", "company2", "marriage2")),
    ]

    husband1 is married to wife1 as seen by the common marriage1 tag
    person1, person3 and wife1 work at the same company.
    …

    The algorithm will try to match people with the least common characteristics
    between them, to maximize entrop— ehm, mingling!

    Have fun."""

    # let's split the persons into groups

    groups = collections.defaultdict(list)
    stats = Statistics()

    for person, tags in people_and_their_tags:
        tags = frozenset(tag.lower() for tag in tags)
        stats.account(tags)
        person= "%s [%s]" % (person, ",".join(tags))
        groups[tags].append(person)

    # shuffle all lists
    for group in groups.values():
        random.shuffle(group)

    output_chain = []
    prev_tags = frozenset()
    while 1:
        next_tags, next_group = stats.most_disjoined(prev_tags, groups)
        output_chain.append(next_group.pop())
        if not next_group:  # it just got empty
            del groups[next_tags]
            if not groups: break
        prev_tags = next_tags

    return output_chain

if __name__ == "__main__":
    example_sequence = [
        ("person1", ("male", "company1")),
        ("person2", ("female", "company2")),
        ("person3", ("male", "company1")),
        ("husband1", ("male", "company2", "marriage1")),
        ("wife1", ("female", "company1", "marriage1")),
        ("husband2", ("male", "company3", "marriage2")),
        ("wife2", ("female", "company2", "marriage2")),
    ]
    print("suggested chain (each person gives present to next person)")
    import pprint
    pprint.pprint(secret_santa(example_sequence))
...