codersincebirthbaby
codersincebirthbaby

Reputation: 35

Is it possible to make this algorithm recursive?

Background

We have a family tradition where my and my siblings' Christmas presents are identified by a code that can be solved using only numbers related to us. For example, the code could be birth month * age + graduation year (This is a simple one). If the numbers were 8 * 22 + 2020 = 2196, the number 2196 would be written on all my Christmas presents.

I've already created a Python class that solves the code with certain constraints, but I'm wondering if it's possible to do it recursively.

Current Code

The first function returns a result set for all possible combinations of numbers and operations that produce a value in target_values

#Master algorithm (Get the result set of all combinations of numbers and cartesian products of operations that reach a target_value, using only the number_of_numbers_in_solution)
#Example: sibling1.results[1] = [(3, 22, 4), (<built-in function add>, <built-in function add>), 29]. This means that 3 + 22 + 4 = 29, and 29 is in target_values
import operator
from itertools import product
from itertools import combinations

NUMBER_OF_OPERATIONS_IN_SOLUTION = 2 #Total numbers involved is this plus 1
NUMBER_OF_NUMBERS_IN_SOLUTION = NUMBER_OF_OPERATIONS_IN_SOLUTION + 1
TARGET_VALUES = {22,27,29,38,39}

def getresults( list ):
  #Add the cartesian product of all possible operations to a variable ops
  ops = []
  opslist = [operator.add, operator.sub, operator.mul, operator.truediv]
  for val in product(opslist, repeat=NUMBER_OF_OPERATIONS_IN_SOLUTION):
    ops.append(val)

  #Get the result set of all combinations of numbers and cartesian products of operations that reach a target_value
  results = []
  for x in combinations(list, NUMBER_OF_NUMBERS_IN_SOLUTION):
      for y in ops:
          result = 0
          for z in range(len(y)):
              #On the first iteration, do the operation on the first two numbers (x[z] and x[z+1])
              if (z == 0):
                  #print(y[z], x[z], x[z+1])
                  result = y[z](x[z], x[z+1])
              #For all other iterations, do the operation on the current result and x[z+1])
              else:
                  #print(y[z], result, x[z+1])
                  result = y[z](result, x[z+1])
          if result in TARGET_VALUES:
            results.append([x, y, result])
            #print (x, y)
  print(len(results))
  return results

Then a class that takes in personal parameters for each person and gets the result set

def getalpha( str, inverse ):
   "Converts string to alphanumeric array of chars"
   array = []
   for i in range(0, len(str)): 
      alpha = ord(str[i]) - 96
      if inverse:
        array.append(27 - alpha)
      else:
        array.append(alpha)
   return array;

class Person:
  def __init__(self, name, middlename, birthmonth, birthday, birthyear, age, orderofbirth, gradyear, state, zip, workzip, cityfirst3):
    #final list
    self.listofnums = []
    self.listofnums.extend((birthmonth, birthday, birthyear, birthyear - 1900, age, orderofbirth, gradyear, gradyear - 2000, zip, workzip))
    self.listofnums.extend(getalpha(cityfirst3, False))
    self.results = getresults(self.listofnums)

Finally, a "solve code" method that takes from the result sets and finds any possible combinations that produce the full list of target_values.

#Compares the values of two sets
def compare(l1, l2):
    result = all(map(lambda x, y: x == y, l1, l2))
    return result and len(l1) == len(l2)

#Check every result in sibling2 with a different result target_value and equal operation sets
def comparetwosiblings(current_values, sibling1, sibling2, a, b):
  if sibling2.results[b][2] not in current_values and compare(sibling1.results[a][1], sibling2.results[b][1]):
    okay = True
    #If the indexes aren't alphanumeric, ensure they're the same before adding to new result set
    for c in range(0, NUMBER_OF_NUMBERS_IN_SOLUTION):
      indexintersection = set([index for index, value in enumerate(sibling1.listofnums) if value == sibling1.results[a][0][c]]) & set([index for index, value in enumerate(sibling2.listofnums) if value == sibling2.results[b][0][c]])
      if len(indexintersection) > 0:
        okay = True
      else:
        okay = False
        break
  else:
    okay = False
  return okay

#For every result, we start by adding the result number to the current_values list for sibling1, then cycle through each person and see if a matching operator list leads to a different result number. (Matching indices as well)
#If there's a result set for everyone that leads to five different numbers in the code, the values will be added to the newresult set
def solvecode( sibling1, sibling2, sibling3, sibling4, sibling5 ):
  newresults = []
  current_values = []

  #For every result in sibling1
  for a in range(len(sibling1.results)):
    current_values = []
    current_values.append(sibling1.results[a][2])
    for b in range(len(sibling2.results)):
      if comparetwosiblings(current_values, sibling1, sibling2, a, b):
        current_values.append(sibling2.results[b][2])
        for c in range(len(sibling3.results)):
          if comparetwosiblings(current_values, sibling1, sibling3, a, c):
            current_values.append(sibling3.results[c][2])
            for d in range(len(sibling4.results)):
              if comparetwosiblings(current_values, sibling1, sibling4, a, d):
                current_values.append(sibling4.results[d][2])
                for e in range(len(sibling5.results)):
                  if comparetwosiblings(current_values, sibling1, sibling5, a, e):
                    newresults.append([sibling1.results[a][0], sibling2.results[b][0], sibling3.results[c][0], sibling4.results[d][0], sibling5.results[e][0], sibling1.results[a][1]])
                current_values.remove(sibling4.results[d][2])
            current_values.remove(sibling3.results[c][2])
        current_values.remove(sibling2.results[b][2])

  print(len(newresults))
  print(newresults)

It's the last "solvecode" method that I'm wondering if I can optimize and make into a recursive algorithm. In some cases it can be helpful to add or remove a sibling, which would look nice recursively (My mom sometimes makes a mistake with one sibling, or we get a new brother/sister-in-law)

Thank you for any and all help! I hope you at least get a laugh out of my weird family tradition.

Edit: In case you want to test the algorithm, here's an example group of siblings that result in exactly one correct solution

#ALL PERSONAL INFO CHANGED FOR STACKOVERFLOW
sibling1 = Person("sibling1", "horatio", 7, 8, 1998, 22, 5, 2020, "ma", 11111, 11111, "red")
sibling2 = Person("sibling2", "liem", 2, 21, 1995, 25, 4, 2018, "ma", 11111, 11111, "pho")
sibling3 = Person("sibling3", "kyle", 4, 21, 1993, 26, 3, 2016, "ma", 11111, 11111, "okl")
sibling4 = Person("sibling4", "jamal", 4, 7, 1991, 29, 2, 2014, "ma", 11111, 11111, "pla")
sibling5 = Person("sibling5", "roberto", 9, 23, 1990, 30, 1, 2012, "ma", 11111, 11111, "boe")

Upvotes: 1

Views: 117

Answers (1)

Vedant36
Vedant36

Reputation: 328

I just spent a while improving the code. Few things I need to mention:

  • It's not good practice to use python keywords(like list, str and zip) as variables, it will give you problems and it makes it harder to debug.
  • I feel like you should use the permutation function as combination gives unordered pairs while permutation gives ordered pairs which are more in number and will give more results. For example, for the sibling info you gave combination gives only 1 solution through solvecode() while permutation gives 12.
  • Because you are working with operators, there can be more cases with brackets. To solve that problem and to make the getresults() function a bit more optimized, I suggest you explore the reverse polish notation. Computerphile has an excellent video on it.
  • You don't need a compare function. list1==list2 works.

Here's the optimized code:

import operator
from itertools import product
from itertools import permutations

NUMBER_OF_OPERATIONS_IN_SOLUTION = 2 #Total numbers involved is this plus 1
NUMBER_OF_NUMBERS_IN_SOLUTION = NUMBER_OF_OPERATIONS_IN_SOLUTION + 1
TARGET_VALUES = {22,27,29,38,39}

def getresults(listofnums):
    #Add the cartesian product of all possible operations to a variable ops
    ops = []
    opslist = [operator.add, operator.sub, operator.mul, operator.truediv]
    for val in product(opslist, repeat=NUMBER_OF_OPERATIONS_IN_SOLUTION):
        ops.append(val)

    #Get the result set of all combinations of numbers and cartesian products of operations that reach a target_value
    results = []
    for x in permutations(listofnums, NUMBER_OF_NUMBERS_IN_SOLUTION):
        for y in ops:
            result = y[0](x[0], x[1])
            if NUMBER_OF_OPERATIONS_IN_SOLUTION>1:
                for z in range(1, len(y)):
                    result = y[z](result, x[z+1])
            if result in TARGET_VALUES:
                results.append([x, y, result])
    return results

def getalpha(string, inverse):
    "Converts string to alphanumeric array of chars"
    array = []
    for i in range(0, len(string)): 
        alpha = ord(string[i]) - 96
        array.append(27-alpha if inverse else alpha)
    return array

class Person:
    def __init__(self, name, middlename, birthmonth, birthday, birthyear, age, orderofbirth, gradyear, state, zipcode, workzip, cityfirst3):
        #final list
        self.listofnums = [birthmonth, birthday, birthyear, birthyear - 1900, age, orderofbirth, gradyear, gradyear - 2000, zipcode, workzip]
        self.listofnums.extend(getalpha(cityfirst3, False))
        self.results = getresults(self.listofnums)

#Check every result in sibling2 with a different result target_value and equal operation sets
def comparetwosiblings(current_values, sibling1, sibling2, a, b):
    if sibling2.results[b][2] not in current_values and sibling1.results[a][1]==sibling2.results[b][1]:
        okay = True
        #If the indexes aren't alphanumeric, ensure they're the same before adding to new result set
        for c in range(0, NUMBER_OF_NUMBERS_IN_SOLUTION):
            indexintersection = set([index for index, value in enumerate(sibling1.listofnums) if value == sibling1.results[a][0][c]]) & set([index for index, value in enumerate(sibling2.listofnums) if value == sibling2.results[b][0][c]])
            if len(indexintersection) > 0:
                okay = True
            else:
                okay = False
                break
    else:
        okay = False
    return okay

And now, the million dollar function or should i say two functions:

# var contains the loop variables a-e, depth keeps track of sibling number
def rec(arg, var, current_values, newresults, depth):
    for i in range(len(arg[depth].results)):
        if comparetwosiblings(current_values, arg[0], arg[depth], var[0], i):
            if depth<len(arg)-1:
                current_values.append(arg[depth].results[i][2])
                rec(arg, var[:depth]+[i], current_values, newresults, depth+1)
                current_values.remove(arg[depth].results[i][2])
            else:
                var.extend([i])
                newresults.append([arg[0].results[var[0]][0], arg[1].results[var[1]][0], arg[2].results[var[2]][0], arg[3].results[var[3]][0], arg[4].results[var[4]][0], arg[0].results[var[0]][1]])

def solvecode(*arg):
    newresults = []
    for a in range(len(arg[0].results)):
        current_values = [arg[0].results[a][2]]
        rec(arg, var=[a], current_values=current_values, newresults=newresults, depth=1)
    print(len(newresults))
    print(newresults)

There is a need for two functions as the first one is the recursive one and the second one is like a packaging. I've also fulfilled your second wish, that was being able to have variable number of siblings' data that can be input into the new solvecode function. I've checked the new functions and they work together exactly like the original solvecode function. Something to be noted is that there is no significant difference in the version's runtimes although the second one has 8 less lines of code. Hope this helped. lmao took me 3 hours.

Upvotes: 1

Related Questions