• # It is possible to diminish this code's run time?

## Question related to mission Family Gifts

After trying to get this program to run faster and use less memory, it is unclear to me what more can be done to have this code complete the challenge in a timely fashion.

```"""\
Every Christmas in my family, each of us gives a present to only one other
person. To organize who should offer a gift to whom (I call it the chain of
presents), we introduced a few rules:

- There should be a single chain where every person in the family is present,
- Couples should not give to one another,
- Every person should give a gift to a different person than they had in
previous years

So the problem would be to find and list the longest list of chains of
presents. For example in a family {a,b,c,d} with couples=({a,b},{c,d}):
[[a,c,b,d],[a,d,b,c]].

You are given a set of family member names and a list of couples in this
family. Find the maximum number of chains with all family members. Gift chains
should be represented as a list of names where i-th gives a present to i+1-th
and the last person in the list to the first.

Input:        Names of family members as a set of strings. Couple list as a
tuple of sets with two strings in each.
Output:       The longest list of gift chains as a list/tuple of lists/tuples
with strings.
Precondition: len(couples) <= 7"""

from operator import itemgetter
from pprint import pprint

Z = frozenset

TABLE = {(Z({'Hollie', 'Gary', 'Jeanette'}),
Z({Z({'Gary', 'Jeanette'})})): 0,
(Z({'Javier', 'Rachel', 'Curtis', 'Lee'}),
Z({Z({'Curtis', 'Lee'}), Z({'Javier', 'Rachel'})})): 2,
(Z({'Mary', 'Selena', 'Phyllis', 'Philip', 'Eric', 'Sondra'}),
Z({Z({'Mary', 'Eric'}), Z({'Philip', 'Sondra'})})): 4,
(Z({'Eugene', 'Delores', 'Delia', 'Ricardo', 'Ella', 'Kurt'}),
Z({Z({'Ella', 'Eugene'}), Z({'Delores', 'Kurt'}),
Z({'Delia', 'Ricardo'})})): 4,
(Z({'Nelson', 'Kaitlin', 'Amelia', 'Jack'}),
Z({Z({'Nelson', 'Amelia'}), Z({'Kaitlin', 'Jack'})})): 2,
(Z({'Eugenia', 'Alex', 'Monique', 'Kitty', 'Robert', 'Tamika', 'Rene',
'Tim', 'Maggie', 'Joseph'}),
Z({Z({'Tim', 'Tamika'}), Z({'Robert', 'Kitty'}),
Z({'Eugenia', 'Alex'}), Z({'Monique', 'Rene'}),
Z({'Maggie', 'Joseph'})})): 8,
(Z({'Melisa', 'Annmarie', 'Dee', 'Rafael', 'Gerald'}),
Z({Z({'Melisa', 'Gerald'}), Z({'Annmarie', 'Rafael'})})): 2,
(Z({'Lula', 'Bill', 'Irene', 'Vincent', 'Dorothea', 'Paulette',
'Virginia'}),
Z()): 6,
(Z({'Tabitha', 'Esperanza', 'Delores', 'Carl', 'Erin', 'Samuel',
'Fred', 'Erica', 'Amber', 'Dixie'}),
Z({Z({'Fred', 'Delores'}), Z({'Erica', 'Carl'})})): 7,
(Z({'Herminia', 'Louis', 'David', 'Sondra', 'Herbert', 'Jane',
'Jeannie', 'Alexandria', 'Theodore', 'Meghan', 'Nettie',
'Eleanor', 'Autumn', 'Jeffery', 'Fay', 'June', 'Lynnette'}),
Z({Z({'Theodore', 'Meghan'}), Z({'David', 'Nettie'}),
Z({'Louis', 'Autumn'}), Z({'Herbert', 'Eleanor'}),
Z({'Fay', 'Jeffery'})})): 14,
(Z({'Fred', 'Yolanda', 'Doreen'}),
Z({Z({'Fred', 'Doreen'})})): 0,
(Z({'Jacklyn', 'Maurice', 'Lela', 'Stella', 'Winnie', 'Barbra',
'Lavonne', 'Estela', 'Gordon'}),
Z({Z({'Maurice', 'Lela'})})): 7,
(Z({'Russell', 'Maryanne', 'Benjamin', 'Matthew', 'Loraine', 'Penny',
'Todd', 'Jenifer', 'Leah'}),
Z({Z({'Jenifer', 'Todd'}), Z({'Benjamin', 'Loraine'}),
Z({'Leah', 'Matthew'})})): 6,
(Z({'Kelly', 'Allison', 'Bobbie', 'Curtis', 'Petra', 'Robin'}),
Z({Z({'Robin', 'Kelly'}), Z({'Curtis', 'Allison'})})): 4}

def main():
"Test the find_chains function with several cases to see if it breaks."
assert checker(find_chains,
{'Gary', 'Jeanette', 'Hollie'},
({'Gary', 'Jeanette'},),
0), "Three of us"
assert checker(find_chains,
{'Curtis', 'Lee', 'Rachel', 'Javier'},
({'Rachel', 'Javier'}, {'Curtis', 'Lee'}),
2), "Pairs"
assert checker(find_chains,
{'Philip', 'Sondra', 'Mary', 'Selena', 'Eric', 'Phyllis'},
({'Philip', 'Sondra'}, {'Eric', 'Mary'}),
4), "Pairs and Singles"
for (family, couples), total in sorted(TABLE.items(), key=itemgetter(1)):
print('total =', total)
assert checker(find_chains, set(family), couples, total), 'Full Test'

def checker(function, family, couples, total):
"Check the function with family and couples to see if it works correctly."
user_result = function(family.copy(), tuple(c.copy() for c in couples))
pprint(user_result)
if (not isinstance(user_result, (list, tuple)) or
any(not isinstance(chain, (list, tuple)) for chain in user_result)):
return False
if len(user_result) < total:
return False
for chain in user_result:
if set(chain) != family or len(chain) != len(family):
return False
for f, s in zip(chain, chain[1:] + [chain[0]]):
if {f, s} in couples:
return False
return False
return True

################################################################################

from collections import Counter
from heapq import heappop, heappush
from itertools import chain, tee

def find_chains(family, couples):
"Return the longest list of gift chains as a list of lists."
target = TABLE[Z(family), Z(map(Z, couples))]
if not target:
return ()
return tuple(map(list, Machine(family, couples, target).search()))

def pairwise(iterable):
a, b = tee(iterable)
next(b, None)
return zip(a, b)

class Machine:

__slots__ = 'family', 'couples', 'target', 'options', 'state'

def __init__(self, family, couples, target):
self.family = family
self.couples = couples
self.target = target
self.options = {name: family - {name} for name in family}
for a, b in couples:
self.options[a].remove(b)
self.options[b].remove(a)
self.state = State((), self.options)

def search(self):
heap, skip = [self.state], {self.state}
while heap:
state = heappop(heap)
for child in state.children:
if len(child.chain) == self.target:
return child.chain
if child not in skip:
heappush(heap, child)
return ()

class State:

__slots__ = 'chain', 'options'

def __init__(self, chain, options):
self.chain = chain
self.options = options

def __lt__(self, other):
return self.score > other.score

def __eq__(self, other):
return set(self.chain) == set(other.chain)

def __hash__(self):
return hash(Z(self.chain))

@property
def score(self):
score = len(self.chain)
score -= Counter(chain(*self.options.values())).most_common(1)[0][1]
return score

@property
def children(self):
for chain in self.combinations_with_refinement(min(self.options)):
yield type(self)(self.chain + (chain,), {start: self.options
[start] - {stop} for start, stop in pairwise(chain + (chain[0],))})

def combinations_with_refinement(self, primer):
yield from self._cwr((primer,), primer, primer, 1, len(self.options))

def _cwr(self, chain, head, tail, size, goal):
size += 1
if size < goal:
for option in self.options[tail]:
if option not in chain:
yield from self._cwr(chain + (option,),
else:
for option in self.options[tail]:
if option not in chain and head in self.options[option]:
yield chain + (option,)

@property
def score(self):
return len(self.chain)

@property
def children(self):
for chain in self.combinations_with_refinement(min(self.options)):
yield type(self)(self.chain + (chain,), self.options)

def combinations_with_refinement(self, primer):
options = self.options
for chain in self.chain:
options = {start: options[start] - {stop} for start, stop in
pairwise(chain + (chain[0],))}
yield from self._cwr((primer,), primer, primer, 1, len(options),
options)

@classmethod
def _cwr(cls, chain, head, tail, size, goal, options):
size += 1
if size < goal:
for option in options[tail]:
if option not in chain:
yield from cls._cwr(chain + (option,), head, option, size,
goal, options)
else:
for option in options[tail]:
if option not in chain and head in options[option]:
yield chain + (option,)

if __name__ == '__main__':
main()
```
25