Secret santa algorithm

前端 未结 9 1406
温柔的废话
温柔的废话 2020-12-04 22:26

Every Christmas we draw names for gift exchanges in my family. This usually involves mulitple redraws until no one has pulled their spouse. So this year I coded up my own

9条回答
  •  醉梦人生
    2020-12-04 22:57

    Python solution here.

    Given a sequence of (person, tags), where tags is itself a (possibly empty) sequence of strings, my algorithm suggests a chain of persons where each person gives a present to the next in the chain (the last person obviously is paired with the first one).

    The tags exist so that the persons can be grouped and every time the next person is chosen from the group most dis-joined to the last person chosen. The initial person is chosen by an empty set of tags, so it will be picked from the longest group.

    So, given an input sequence of:

    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")),
    ]
    

    a suggestion is:

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

    Of course, if all persons have no tags (e.g. an empty tuple) then there is only one group to choose from.

    There isn't always an optimal solution (think an input sequence of 10 women and 2 men, their genre being the only tag given), but it does a good work as much as it can.

    Py2/3 compatible.

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

提交回复
热议问题