关于语言不可知论:秘密圣诞老人算法

Secret santa algorithm

每年圣诞节,我们都会为家里的礼物交换画名字。这通常涉及多次重绘,直到没有人拉他们的配偶。所以今年我编写了自己的名字绘图应用程序,它接收一堆名字,一堆不允许的配对,然后向每个人发送一封电子邮件给他们选择的礼物。

现在,算法是这样工作的(伪代码):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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

有谁知道更多关于图论的人知道某种算法在这里会更好地工作吗?就我的目的而言,这是可行的,但我很好奇。

编辑:由于电子邮件不久前发出,我只是希望能学到一些东西,我将把它改写为一个图论问题。我对排除都是对的特殊情况不感兴趣(比如配偶没有得到对方)。我对有足够的排除项以找到任何解决方案成为困难部分的情况更感兴趣。我上面的算法只是一个简单的贪心算法,我不确定在所有情况下都会成功。

从一个完整的有向图和一个顶点对列表开始。对于每个顶点对,删除从第一个顶点到第二个顶点的边。

目标是得到一个图,其中每个顶点都有一条边进入,一条边离开。


如果允许他们分享礼物,只需制作一个连接两个人的边图,然后使用完美匹配算法。 (为(聪明的)算法寻找"路径、树木和花朵")


我自己只是这样做,最后我使用的算法并不能完全模拟绘图名称,但它非常接近。基本上打乱列表,然后将每个人与列表中的下一个人配对。从帽子中抽出名字的唯一区别是,你得到一个循环,而不是可能得到只相互交换礼物的迷你小组。如果有什么可能是一个功能。

在 Python 中的实现:

1
2
3
4
5
6
7
8
9
10
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))

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

作为奖励,如果您想进行这种无记名投票方式,请打印第一个列表中的信封和第二个列表中的姓名。装信封时不要偷看。 (或者您可以自动向他们挑选的每个人发送电子邮件。)

这个问题还有更多的解决方案。


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


图论中有一个称为哈密顿回路的概念,它描述了您描述的"目标"。任何发现这一点的人的一个提示是告诉用户使用哪个"种子"来生成图形。这样,如果您必须重新生成图表,您可以。如果您必须添加或删除一个人,"种子"也很有用。在这种情况下,只需选择一个新的"种子"并生成一个新图表,确保告诉参与者哪个"种子"是当前/最新的。


创建一个图,其中每条边都是"giftability",代表配偶的顶点不会相邻。随机选择一条边(即礼物分配)。删除所有来自送礼者的边和所有去往接收者的边并重复。


这里是神秘圣诞老人问题的简单java实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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));
    }
}

我刚刚创建了一个可以做到这一点的网络应用程序 - http://www.secretsantaswap.com/

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

操作如下:
1.为所有参与者分配一个唯一标识符,记住他们在哪个子组
2. 复制并打乱该列表(目标)
3. 创建每个子组中参与者数量的数组
4. 复制 [3] 中的数组作为目标
5. 创建一个新数组来保存最终匹配
6. 遍历分配不符合以下任何条件的第一个目标的参与者:
A. 参与者 == 目标
B. 参与者.子组 == 目标.子组
C. 选择目标将导致子组在未来失败(例如,子组 1 的剩余非子组 1 目标必须始终至少与剩余的参与者子组 1 参与者一样多)
D. 参与者(n 1) == 目标 (n 1)
如果我们分配目标,我们也会将数组从 3 和 4

递减

所以,(一点也不)漂亮,但它确实有效。希望对你有帮助,

丹·卡尔森


这里是 Python 解决方案。

给定一个 (person, tags) 序列,其中标签本身是一个(可能是空的)字符串序列,我的算法建议一个人链,其中每个人给链中的下一个人礼物(最后一个人显然是成对的与第一个)。

标签的存在是为了可以对人进行分组,并且每次从最不加入的组中选择下一个人时,选择最后一个人。最初的人是由一组空标签选择的,因此将从最长的组中选择。

所以,给定一个输入序列:

1
2
3
4
5
6
7
8
9
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 [男,company1]\\',
\\'person2 [female,company2]\\',
\\'person3 [男,company1]\\',
\\'wife2 [female,marriage2,company2]\\',
\\'老公1 [男,婚姻1,公司2]\\',
\\'老公2 [男,婚姻2,公司3]\\',
\\'wife1 [female,marriage1,company1]\\']

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

并不总是有最佳解决方案(想想 10 名女性和 2 名男性的输入序列,他们的类型是唯一给出的标签),但它尽其所能。

Py2/3 兼容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
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))