diff --git a/README.md b/README.md index 425a355..31e2235 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ By default, the time complexity indicates the worst time and the space complexit #### [⬆](#toc) Array: * [Calculate the rotation distance for a sorted rotated array](problem/Array/Rotatearraydistance.cpp) -* Celebrity problem +* [Celebrity problem](problem/Array/CelebrityProblem.py) * [Detect cycle in an array](problem/Array/Detect%20cycle%20in%20an%20array.cpp) * [Detect the longest cycle in an array](problem/Array/Detect%20the%20longest%20cycle%20in%20an%20array.cpp) * [Diagonal elements sum of a spiral matrix](problem/Array/Diagonal%20elements%20sum%20of%20spiral%20matrix.cpp) diff --git a/problem/Array/CelebrityProblem.py b/problem/Array/CelebrityProblem.py new file mode 100644 index 0000000..9c71537 --- /dev/null +++ b/problem/Array/CelebrityProblem.py @@ -0,0 +1,163 @@ +# /usr/bin/env python2.7 +# vim: set fileencoding=utf-8 +""" +In a party of N people, only one person (the celebrity) is known to everyone. +Such a person may be present in the party. If yes, they don't know anyone else +at the party. + +We can only ask whether "does person-A know person-B": + + HasAcquaintance(A, B) returns True if A knows B, False otherwise. + +Find the celebrity at the party (if they exist) in as few calls to +HasAcquaintance as possible. +""" + + +class _BaseSolution: + def __init__(self, edges=None): + self.edges = edges or {} + self.has_acquaintance_counter = 0 + self.answer = None + self._called = False + + def HasAcquaintance(self, A, B): + """Returns True if A knows B.""" + self.has_acquaintance_counter += 1 + return B in self.edges.get(A, ()) + + def Solve(self, people=None): + if people is None: + people = self.edges.keys() + self.answer = self.findTheCelebrity(people) + return self + + def findTheCelebrity(self, people): + """Return the name of the celebrity if they are at the party. + + Return None if there is no celebrity at the party. + """ + return None + + +class _TestData: + """Struct for containing test data and expected celebrity value.""" + def __init__(self, name, expected, edges): + self.name = name + self.expected = expected + self.edges = edges + + +class NSquaredSolution(_BaseSolution): + """Assume everyone is the celebrity and disqualify them if they can't be + the celebrity: + + (1) If they know anyone else in the party, they can't be the celebrity. + + (2) If they are not known by everyone else in the party, they also cannot be + the celebrity. + + If there is an entry remaining in the list of potential celebrities, then + that is the celebrity. It's an error if there is more than one potential + celebrity. + """ + def findTheCelebrity(self, people): + import collections + # each key knows everyone in their matching value + social_graph = collections.defaultdict(set) + + possible_celebrities = set(people) + for A in people: + for B in [p for p in people if p != A]: + # (1) celebrities don't know anyone at the party + if self.HasAcquaintance(A, B): + social_graph[A].add(B) + possible_celebrities.discard(A) + if self.HasAcquaintance(B, A): + social_graph[B].add(A) + possible_celebrities.discard(B) + + # (2) celebrities are known by everyone at the party + celebrities = set(possible_celebrities) + for celeb in possible_celebrities: + for edges in social_graph.values(): + if celeb not in edges: + celebrities.discard(celeb) + break + + if celebrities: + # we assume there's only one celebrity + return celebrities.pop() + + return None + + +class TwoPointersSolution(_BaseSolution): + def findTheCelebrity(self, people): + a_idx = 0 + b_idx = len(people) - 1 + while a_idx < b_idx: + if self.HasAcquaintance(people[a_idx], people[b_idx]): + # if A knows B, then A can't be the celebrity + a_idx += 1 + else: + # if A doesn't know B, then B can't be the celebrity + b_idx -= 1 + + # double check that A meets the celebrity requirements for everyone. + # we may have missed a disqualification when we aggressively walked + # people in the last step + A = people[a_idx] + for person in [p for p in people if p != A]: + if self.HasAcquaintance(A, person): + # if A knows someone at the party, they can't be the celebrity + return None + if not self.HasAcquaintance(person, A): + # if someone doesn't know A, then A can't be the celebrity + return None + return A + + +def main(): + test_data = [ + _TestData('D_IS_THE_CELEBRITY', + 'd', + {'a': ('b', 'c', 'd'), + 'b': ('c', 'd'), + 'c': ('a', 'd'), + 'd': (), + 'e': ('b', 'd')}), + _TestData('NO_CELEBRITY', + None, + {'a': ('b', 'c', 'd'), + 'b': ('c', 'd'), + 'c': ('a', 'd'), + 'd': ('e'), + 'e': ('b', 'd')}), + _TestData('ALMOST_A_CELEBRITY', + None, + {'a': ('b', 'c', 'd'), + 'b': ('c', 'd'), + 'c': ('a', 'd'), + 'd': (), + 'e': ('b')}), # e has never heard of d + ] + solutions = [NSquaredSolution, TwoPointersSolution] + + for test_case in test_data: + print + print test_case.name, test_case.edges + for solution in solutions: + s = solution(test_case.edges).Solve() + s_repr = '%s(%d)' % (s.__class__.__name__, + s.has_acquaintance_counter) + if s.answer == test_case.expected: + print 'PASS %s: %s' % (s_repr, s.answer) + else: + print 'FAIL %s: (expected %s, got %s)' % (s_repr, + test_case.expected, + s.answer) + + +if __name__ == '__main__': + main()