From 8d44cd62541c1cd23eb18194bc4d05f80f37375b Mon Sep 17 00:00:00 2001 From: Tony Campbell Date: Mon, 26 Jun 2017 13:10:25 -0400 Subject: [PATCH 01/10] Initial version of Celebrity Problem - define problem - create a sample HasAcquaintance() method --- problem/Array/CelebrityProblem.py | 34 +++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 problem/Array/CelebrityProblem.py diff --git a/problem/Array/CelebrityProblem.py b/problem/Array/CelebrityProblem.py new file mode 100644 index 0000000..9d1f161 --- /dev/null +++ b/problem/Array/CelebrityProblem.py @@ -0,0 +1,34 @@ +# /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. +""" + + +def HasAcquaintance(A, B): + """d is the celebrity.""" + edges = { + 'a': ('b', 'c', 'd'), + 'b': ('c', 'd'), + 'c': ('a', 'd'), + 'd': (), + 'e': ('b', 'd'), + } + return B in edges.get(A, ()) + + +def main(): + pass + + +if __name__ == '__main__': + main() From ad661a764949859875ef19e7656ec2077d253bac Mon Sep 17 00:00:00 2001 From: Tony Campbell Date: Mon, 26 Jun 2017 13:15:42 -0400 Subject: [PATCH 02/10] Add link to solution of celebrity problem --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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) From bb5db0c8edd8d2b19e30b8dcced8dba0375ea89b Mon Sep 17 00:00:00 2001 From: Tony Campbell Date: Mon, 26 Jun 2017 14:00:48 -0400 Subject: [PATCH 03/10] Add counter to HasAcquaintance() --- problem/Array/CelebrityProblem.py | 44 +++++++++++++++++++++++-------- 1 file changed, 33 insertions(+), 11 deletions(-) diff --git a/problem/Array/CelebrityProblem.py b/problem/Array/CelebrityProblem.py index 9d1f161..5e32b9d 100644 --- a/problem/Array/CelebrityProblem.py +++ b/problem/Array/CelebrityProblem.py @@ -14,20 +14,42 @@ """ -def HasAcquaintance(A, B): - """d is the celebrity.""" - edges = { - 'a': ('b', 'c', 'd'), - 'b': ('c', 'd'), - 'c': ('a', 'd'), - 'd': (), - 'e': ('b', 'd'), - } - return B in edges.get(A, ()) +D_IS_THE_CELEBRITY = { + 'a': ('b', 'c', 'd'), + 'b': ('c', 'd'), + 'c': ('a', 'd'), + 'd': (), + 'e': ('b', 'd'), +} + + +class Solution: + def __init__(self, edges=None): + self.edges = edges or {} + self.has_acquaintance_counter = 0 + + def HasAcquaintance(self, A, B): + """Returns True if A knows B.""" + self.has_acquaintance_counter += 1 + return B in self.edges.get(A, ()) + + def __call__(self, people=None): + if people is None: + people = self.edges.keys() + celebrity = self.findTheCelebrity(people) + return self.has_acquaintance_counter, celebrity + + 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 def main(): - pass + s = Solution(D_IS_THE_CELEBRITY) + print s() if __name__ == '__main__': From 5933813a93ec04f408bb98d353b12e212317714b Mon Sep 17 00:00:00 2001 From: Tony Campbell Date: Mon, 26 Jun 2017 15:50:27 -0400 Subject: [PATCH 04/10] Add test data for party with no celebrity --- problem/Array/CelebrityProblem.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/problem/Array/CelebrityProblem.py b/problem/Array/CelebrityProblem.py index 5e32b9d..2cd468f 100644 --- a/problem/Array/CelebrityProblem.py +++ b/problem/Array/CelebrityProblem.py @@ -22,6 +22,14 @@ 'e': ('b', 'd'), } +NO_CELEBRITY = { + 'a': ('b', 'c', 'd'), + 'b': ('c', 'd'), + 'c': ('a', 'd'), + 'd': ('e'), + 'e': ('b', 'd'), +} + class Solution: def __init__(self, edges=None): From af70b4b572b95a026747954ac2b3c0f4015c4b3f Mon Sep 17 00:00:00 2001 From: Tony Campbell Date: Mon, 26 Jun 2017 15:51:57 -0400 Subject: [PATCH 05/10] Create base class for solution to make testing easier This will allow us to compare different solution types --- problem/Array/CelebrityProblem.py | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/problem/Array/CelebrityProblem.py b/problem/Array/CelebrityProblem.py index 2cd468f..c110884 100644 --- a/problem/Array/CelebrityProblem.py +++ b/problem/Array/CelebrityProblem.py @@ -31,21 +31,32 @@ } -class Solution: +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 __call__(self, people=None): + def Solve(self, people=None): if people is None: people = self.edges.keys() celebrity = self.findTheCelebrity(people) - return self.has_acquaintance_counter, celebrity + self.answer = celebrity + return self.has_acquaintance_counter, self.answer + + def __str__(self): + if not self._called: + self.Solve() + return '%s => %s (%d calls to HasAcquaintance)' % ( + self.__class__.__name__, + self.answer, + self.has_acquaintance_counter) def findTheCelebrity(self, people): """Return the name of the celebrity if they are at the party. @@ -55,9 +66,13 @@ def findTheCelebrity(self, people): return None +class NSquaredSolution(_BaseSolution): + pass def main(): - s = Solution(D_IS_THE_CELEBRITY) - print s() + for test_case in (D_IS_THE_CELEBRITY, NO_CELEBRITY): + print + print test_case + print NSquaredSolution(test_case) if __name__ == '__main__': From 620d54f23a31275247b1777ccb3ce07c4dcc2de1 Mon Sep 17 00:00:00 2001 From: Tony Campbell Date: Mon, 26 Jun 2017 16:08:41 -0400 Subject: [PATCH 06/10] Add a straightforward O(N^2) solution --- problem/Array/CelebrityProblem.py | 43 ++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/problem/Array/CelebrityProblem.py b/problem/Array/CelebrityProblem.py index c110884..3f618b1 100644 --- a/problem/Array/CelebrityProblem.py +++ b/problem/Array/CelebrityProblem.py @@ -67,7 +67,48 @@ def findTheCelebrity(self, people): class NSquaredSolution(_BaseSolution): - pass + """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 + for _, celeb in enumerate(possible_celebrities): + for edges in social_graph.values(): + if celeb not in edges: + possible_celebrities.discard(celeb) + break + + if possible_celebrities: + # we assume there's only one celebrity + return possible_celebrities.pop() + + return None + + def main(): for test_case in (D_IS_THE_CELEBRITY, NO_CELEBRITY): print From b4c34b9399008e80571e3825a56891d4f05d5457 Mon Sep 17 00:00:00 2001 From: Tony Campbell Date: Mon, 26 Jun 2017 17:16:09 -0400 Subject: [PATCH 07/10] Add test harness for multiple solution options --- problem/Array/CelebrityProblem.py | 59 +++++++++++++++++++------------ 1 file changed, 36 insertions(+), 23 deletions(-) diff --git a/problem/Array/CelebrityProblem.py b/problem/Array/CelebrityProblem.py index 3f618b1..77f863f 100644 --- a/problem/Array/CelebrityProblem.py +++ b/problem/Array/CelebrityProblem.py @@ -14,23 +14,6 @@ """ -D_IS_THE_CELEBRITY = { - 'a': ('b', 'c', 'd'), - 'b': ('c', 'd'), - 'c': ('a', 'd'), - 'd': (), - 'e': ('b', 'd'), -} - -NO_CELEBRITY = { - 'a': ('b', 'c', 'd'), - 'b': ('c', 'd'), - 'c': ('a', 'd'), - 'd': ('e'), - 'e': ('b', 'd'), -} - - class _BaseSolution: def __init__(self, edges=None): self.edges = edges or {} @@ -46,9 +29,8 @@ def HasAcquaintance(self, A, B): def Solve(self, people=None): if people is None: people = self.edges.keys() - celebrity = self.findTheCelebrity(people) - self.answer = celebrity - return self.has_acquaintance_counter, self.answer + self.answer = self.findTheCelebrity(people) + return self def __str__(self): if not self._called: @@ -66,6 +48,14 @@ def findTheCelebrity(self, people): 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: @@ -110,10 +100,33 @@ def findTheCelebrity(self, people): def main(): - for test_case in (D_IS_THE_CELEBRITY, NO_CELEBRITY): + 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')}), + ] + solutions = [NSquaredSolution] + + for test_case in test_data: print - print test_case - print NSquaredSolution(test_case) + print test_case.name + for solution in solutions: + s = solution(test_case.edges).Solve() + if s.answer == test_case.expected: + print 'Correct: %s' % (s,) + else: + print 'Wrong(%s != %s): %s' % (test_case.expected, s.answer, s) if __name__ == '__main__': From 904aa53a2c4248eb83842aad9f850bc2c4ccf46e Mon Sep 17 00:00:00 2001 From: Tony Campbell Date: Mon, 26 Jun 2017 17:54:39 -0400 Subject: [PATCH 08/10] Add test for almost celebrity status --- problem/Array/CelebrityProblem.py | 32 +++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/problem/Array/CelebrityProblem.py b/problem/Array/CelebrityProblem.py index 77f863f..d28f8b8 100644 --- a/problem/Array/CelebrityProblem.py +++ b/problem/Array/CelebrityProblem.py @@ -32,14 +32,6 @@ def Solve(self, people=None): self.answer = self.findTheCelebrity(people) return self - def __str__(self): - if not self._called: - self.Solve() - return '%s => %s (%d calls to HasAcquaintance)' % ( - self.__class__.__name__, - self.answer, - self.has_acquaintance_counter) - def findTheCelebrity(self, people): """Return the name of the celebrity if they are at the party. @@ -86,15 +78,16 @@ def findTheCelebrity(self, people): possible_celebrities.discard(B) # (2) celebrities are known by everyone at the party - for _, celeb in enumerate(possible_celebrities): + celebrities = set(possible_celebrities) + for celeb in possible_celebrities: for edges in social_graph.values(): if celeb not in edges: - possible_celebrities.discard(celeb) + celebrities.discard(celeb) break - if possible_celebrities: + if celebrities: # we assume there's only one celebrity - return possible_celebrities.pop() + return celebrities.pop() return None @@ -115,6 +108,13 @@ def main(): '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] @@ -123,10 +123,14 @@ def main(): print test_case.name 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 'Correct: %s' % (s,) + print 'PASS %s: %s' % (s_repr, s.answer) else: - print 'Wrong(%s != %s): %s' % (test_case.expected, s.answer, s) + print 'FAIL %s: (expected %s, got %s)' % (s_repr, + test_case.expected, + s.answer) if __name__ == '__main__': From 5123ab57e232f059a6a77c915f4c0243d689d251 Mon Sep 17 00:00:00 2001 From: Tony Campbell Date: Mon, 26 Jun 2017 17:55:27 -0400 Subject: [PATCH 09/10] Show edges when testing --- problem/Array/CelebrityProblem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/problem/Array/CelebrityProblem.py b/problem/Array/CelebrityProblem.py index d28f8b8..9d10d36 100644 --- a/problem/Array/CelebrityProblem.py +++ b/problem/Array/CelebrityProblem.py @@ -120,7 +120,7 @@ def main(): for test_case in test_data: print - print test_case.name + print test_case.name, test_case.edges for solution in solutions: s = solution(test_case.edges).Solve() s_repr = '%s(%d)' % (s.__class__.__name__, From 1efc400f64af945e38f694a43960b9c612445001 Mon Sep 17 00:00:00 2001 From: Tony Campbell Date: Mon, 26 Jun 2017 18:01:59 -0400 Subject: [PATCH 10/10] Add two-pointers solution Read about this approach on a coding site. --- problem/Array/CelebrityProblem.py | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/problem/Array/CelebrityProblem.py b/problem/Array/CelebrityProblem.py index 9d10d36..9c71537 100644 --- a/problem/Array/CelebrityProblem.py +++ b/problem/Array/CelebrityProblem.py @@ -92,6 +92,32 @@ def findTheCelebrity(self, people): 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', @@ -116,7 +142,7 @@ def main(): 'd': (), 'e': ('b')}), # e has never heard of d ] - solutions = [NSquaredSolution] + solutions = [NSquaredSolution, TwoPointersSolution] for test_case in test_data: print