秘密圣诞老人算法

时间:2008-11-07 20:34:28

标签: algorithm language-agnostic graph-theory

每年圣诞节,我们都会为家人的礼物交换画出名字。这通常涉及多次重绘,直到没有人拉他们的配偶。因此,今年我编写了自己的名字绘图应用程序,其中包含一系列名称,一堆不允许的配对,并向所有与其所选礼品的人发送电子邮件。

现在,算法的工作原理如下(伪代码):

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 个答案:

答案 0 :(得分:9)

如果允许他们分享礼物然后使用完美的匹配算法,只需创建一个边缘连接两个人的图表。 (为(聪明的)算法寻找“路径,树和花”)

答案 1 :(得分:6)

我不会使用不允许的配对,因为这会大大增加问题的复杂性。只需将每个人的姓名和地址输入一个列表即可。创建列表的副本并保持混洗,直到两个列表的每个位置中的地址不匹配。这将确保没有人得到自己或他们的配偶。

作为奖励,如果你想做这个秘密选票风格,请打印第一个列表中的信封和第二个列表中的名称。塞满信封时不要偷看。 (或者你可以自动通过电子邮件发送给所有人。)

this thread还有更多解决方案可以解决这个问题。

答案 2 :(得分:6)

我自己就是这样做的,最后我使用的算法并没有用帽子精确地模拟绘图名称,但它非常接近。基本上将列表洗牌,然后将每个人与列表中的下一个人配对。与帽子名称的唯一区别在于,您获得一个周期,而不是可能获得仅与彼此交换礼物的迷你子组。如果有什么可能是一个功能。

在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))

答案 3 :(得分:5)

嗯。我参加了图论的课程,但更简单的是随机地置换你的列表,对每个连续的组配对,然后交换任何不允许的元素与另一个。由于任何给定对中没有不允许的人,如果您不允许与所选组进行交换,则交换将始终成功。你的算法太复杂了。

答案 4 :(得分:2)

创建一个图形,其中每条边都是“giftability”代表配偶的顶点不会相邻。随机选择边缘(即礼品分配)。删除来自礼品夹的所有边缘以及所有到达接收器的边缘并重复。

答案 5 :(得分:2)

图论中有一个名为Hamiltonian Circuit的概念描述了你描述的“目标”。找到这个的任何人的一个提示是告诉用户使用哪个“种子”来生成图表。这样,如果你必须重新生成图表。如果您必须添加或删除某人,“种子”也很有用。在这种情况下,只需选择一个新的“种子”并生成一个新的图表,确保告诉参与者哪个“种子”是当前/最新的种子。

答案 6 :(得分:1)

我刚创建了一个可以完成此操作的网络应用 - http://www.secretsantaswap.com/

我的算法允许子组。它不漂亮,但它有效。

操作如下:
1。为所有参与者分配唯一标识符,记住他们所在的子组
2。复制并随机播放该列表(目标)
3。创建每个子组中参与者数量的数组
4。来自[3]的重复数组用于目标
5。创建一个新数组来保存最终匹配
6。迭代参与者分配不符合以下任何条件的第一个目标:
A.参与者==目标
B.参与者。子组== target.Subgroup
C.选择目标将导致子组在未来失败(例如,子组1必须始终至少有剩余的非子组1目标作为参与者子组1参与者剩余)
D.参与者(n + 1)==目标(n + 1)
如果我们分配目标,我们也将数组从3和4递减

所以,不是很漂亮(根本没有),但它确实有效。希望它有所帮助,

丹·卡尔森

答案 7 :(得分:1)

这是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));
    }
}

答案 8 :(得分:0)

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 [female,company2]',  'person3 [男,公司1]',  'wife2 [female,marriage2,company2]',  'husband1 [男,婚姻1,公司2]',  'husband2 [男,婚姻2,公司3]',  'wife1 [female,marriage1,company1]']

当然,如果所有人都没有标签(例如空元组),那么只有一个组可供选择。

并不总是有一个最佳解决方案(想想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))