Previous Section Table of Contents Next Section

Assign Gift Givers

This function is for those situations where a set of people, such as office coworkers, exchange gifts among themselves, with each person assigned to buy gifts for one other person.

This function walks the list of people and assigns a gift target to each of them. It has to ensure that no one is assigned his or her own name. In particular, with a list of N people, it needs to avoid the situation where the first N-1 people are chosen to give gifts among themselves and, when it is time to process the last person on the list, no one else is left.

The program is passed as a parameter a list of names, and returns a dictionary. In the dictionary, the keys are the same list of names, and the values are the name that each person should give a gift to.

For a Python list, the function


mylist.count(x)


returns the number of times that x appears in mylist.

The function


mylist.pop(i)


returns the element at index i of mylist, and also removes it from the list.

The function


mylist.index(x)


returns the index of the first occurrence of x in mylist. It's an error if x does not occur in mylist.

Source Code


 1.      import random

 2.

 3.      def secretSanta( inputList ):

 4.      """

 5.          inputList: A list of names

 6.

 7.          Returns a dictionary, the keys are givers' names,

 8.               the values are receivers' names.

 9.        """

10.

11.        if len(inputList) < 2:

12.          return {}

13.

14.        returnDict = {}

15.

16.        """ Make a copy of the input list; we remove people

17.           from this list as they are assigned givers.

18.        """

19.

20.        receiversList = inputList[:]

21.

22.        for person in inputList:

23.

24.          """ If there are only two receivers left, and one

25.              of them is the last person in inputList, then

26.              assign the last person to the second-to-last

27.              person.

28.          """

29.

30.          if len(receiversList) == 2:

31.            if receiversList.count(inputList[-1]) == 1:

32.              returnDict[person] = inputList[-1]

33.              returnDict[inputList[-1]] = person

34.              break;

35.

36.          """ The typical situation, just randomly pick

37.              someone out of receiversList and give them to

38.              person. We don't want to assign someone to

39.              themselves. If that happens, we assign them the

40.              next person in receiversList.

41.          """

42.

43.          if receiversList.count(person) == 1:

44.            receiverIndex = \

45.                int ((len(receiversList)-1) * random.random())

46.            if receiversList.index(person) <= receiverIndex:

47.              receiverIndex += 1

48.          else:

49.            receiverIndex = \

50.                int (len(receiversList) * random.random())

51.

52.          returnDict[person] = \

53.              receiversList.pop(receiverIndex);

54.

55.       return returnDict


Suggestions

  1. Two lists (inputList and receiversList) and one dictionary (returnDict) are used. State the goal of each one, and note which ones are modified and where.

  2. On line 46, the index() function is used to look up person in receiverList. This implies that person must be in receiverList. Is this guaranteed to be true?

  3. An invariant condition exists between returnDict and receiversList, which is that an element in one is not in the other. Check the sections of code that relate to this to ensure that the condition is always true.

  4. What is the goal of the code on lines 43-50? How many different paths to walk through exist in this code?

Hints

  1. Walk through one iteration of the code when the function was passed an inputList equal to [ "Tom", "Joe", "Donna", "Susan", "Paul" ].

  2. Imagine the same input list, but the next iteration of the main for loop on line 22, where person is "Joe", and assume the remaining receiversList is [ "Tom", "Donna", "Susan", "Paul" ].

  3. Consider the fourth iteration, where person is "Susan", and assume receiversList is now [ "Donna", "Paul" ].

Explanation of the Bug

The code that appears on line 33


returnDict[inputList[-1]] = person


has a bug. Although the code looks nice and symmetric with the code on line 32, it is not correct. This is shown by walking through the third hint.

The code assumes that if the main for loop has two people left to iterate for, the receiversList has those same two people on it. So, if you have Susan and Paul left, the assumption is that receiversList will be [ "Susan", "Paul" ], and you can just assign them to each other.

In fact, this is not always true (and if it does happen to be true, the standard logic on lines 43-50 will handle it correctly anyway). The problem that lines 30-34 is trying to avoid is something shown in the third hint: person is "Susan", Paul is the next (and last) person to be processed by the main for loop, and Paul is on receiversList with someone other than Susan (in the example shown in the third hint, Donna and Paul are on receiversList). What might happen is that Susan is assigned Donna; in this case, Paul is stuck with himself.

This is an A.logic error. Line 32 is correct because it creates a "Susan" : "Paul" element in returnDict. But, line 33 should be creating "Paul" : "Donna", not "Paul" : "Susan". How can the code accomplish this? We know that Paul is the last person in receiversList because he was last in inputList and receiversList originated as a copy of inputList. Because receiversList has two elements, and "Paul" is the last element, "Donna" must be the first one. So, line 33 should read as follows:


returnDict[inputList[-1]] = receiversList[0]


    Previous Section Table of Contents Next Section