【问题标题】:Secret santa algorithm秘密圣诞老人算法
【发布时间】:2010-09-21 09:00:00
【问题描述】:

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

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

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

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

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

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

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

【问题讨论】:

    标签: algorithm language-agnostic graph-theory


    【解决方案1】:

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

    【讨论】:

    • 不完全是:它是一个有向图,你不一定想要完美匹配;您只需要一个子图,使得每个顶点的入度 = 出度 = 1 - 循环覆盖。 [有办法将其简化为完美匹配问题,但也有办法直接解决。]
    • @ShreevatsaR:图表不一定是定向的。只需掷硬币为每个周期选择一个方向。 (这是假设所有列入黑名单的对都是对称的。)
    • P.S.将图视为无向可排除任何 2 循环,这可能是可取的。
    • 如果黑名单对不对称怎么办?有算法吗?
    【解决方案2】:

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

    在 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】:

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

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

      this thread 上还有更多解决此问题的方法。

      【讨论】:

      • 程序只是向所有人发送一封电子邮件,所以保密问题不是问题。
      • 这是我提到的选项之一。
      【解决方案4】:

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

      【讨论】:

      • 此人和他们的配偶都将被禁止,因此不能保证交换工作。
      • 不正确,因为选择的组将包含此人和他们的配偶(否则不需要交换)。
      【解决方案5】:

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

      【讨论】:

      • 这不会造成不完美的结果吗?如果送礼者有首选的送礼人怎么办?
      【解决方案6】:

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

      【讨论】:

        【解决方案7】:

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

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

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

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

        丹·卡尔森

        【讨论】:

          【解决方案8】:

          这里是秘密圣诞老人问题的简单 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));
              }
          }
          

          【讨论】:

            【解决方案9】:

            这里是 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 [男,company1]', 'person2 [female,company2]', 'person3 [男,company1]', 'wife2 [female,marriage2,company2]', '丈夫1 [男,婚姻1,公司2]', '丈夫2 [男,婚姻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))
            

            【讨论】:

              猜你喜欢
              • 2022-01-12
              • 2013-11-10
              • 2016-05-23
              • 2022-01-15
              • 1970-01-01
              • 2012-01-26
              • 2022-11-17
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多