0

I have written this following code that takes an integer N and and array of integers: arr. The task is to find the shortest combination of the integers in arr that sum up to N . Each elements of arr can be used as times as you want. For example 9, [3,2] should produce the result [3,3,3] instead of [2,2,2,3] as the former combination is of shorter length( lesser number of elements).

def bestSum(n, arr, memo={}):
    key=str(n)
    if key in memo.keys():
        return memo[key]
    if n<0 :
        return None
    if n==0:
        return []

    best_solution = None
    for i in arr:
        solution= bestSum(n-i,arr,memo)
        if (solution is not None):
            combination = list(k for k in solution)
            combination.append(i)
            if((best_solution is None ) or (len(combination)<len(best_solution))):
                best_solution=combination
    memo[key] =best_solution
    return best_solution



print(bestSum(8,[3,2]))
print(bestSum(8, [4,2]))

The output of first print statement is [2,3,3] which is correct. But the second print statement also produced [2, 3, 3] which is not possible as there is no 3 in arr in this case. The output should have been [4,4]

Now if I remove 1st statement and execute only the second statement, then It produces correct result => [4,4]

Again, if I change the order like-


print(bestSum(8, [4,2]))
print(bestSum(8,[3,2]))

both these two produces [4,4]

It seems as if the memo created in print(bestSum(8, [4,2])) is existing even after completion of returning the value to first print and the second call print(bestSum(8, [3,2])) is using the same memo.

I can't understand why is it happening so? shouldn't the first memo object be automatically erased after completion of first print ? Shouldn't a new memo be created in the second call?

If memo continues to exist after completion of 1st statement it should be able to be accessed outside of that statement like print(memo) But this is not the case

wjandrea
  • 28,235
  • 9
  • 60
  • 81
  • You are correct, the dictionary `memo` is a mutable default argument to the `bestSum()` function. It is shared among all calls to the function, even the second call, separate one, i.e. `print(bestSum(8, [4,2]))` You might wanna initialize the default argument in the following way: ``` def bestSum(n, arr, memo=None): if memo is None: memo = {} \\... rest of the logic ``` Tested it, seems to work for the two inputs you provided. – Ahsan Tarique Aug 06 '23 at 17:08
  • Thank You so much, Learnt a new fact !!! Is it true for python only ? In JavaScript the same code works just fine even with a mutable default argument ( a JavaScript Object). – Non_CS_Dude Aug 06 '23 at 17:14

1 Answers1

0

In Python, default arguments are evaluated when the function is defined, not each time the function is called. This means that if you use a mutable default argument and mutate it, the mutated object will be used whenever the function is called.

So in your function, memo={} is only evaluated once, when the function is defined. That's why when you call bestSum the second time, it uses the same memo dictionary from the first call, not a new empty dictionary. This is why the second call is returning the incorrect result, because it is utilizing the memo from the first call.

A common Python idiom to avoid this issue is to use None as the default argument value, and then set the actual default value inside the function. Here's how you could modify your function:

def bestSum(n, arr, memo=None):
    if memo is None:
        memo = {}
    key=str(n)
    if key in memo.keys():
        return memo[key]
    if n<0 :
        return None
    if n==0:
        return []

    best_solution = None
    for i in arr:
        solution= bestSum(n-i,arr,memo)
        if (solution is not None):
            combination = list(k for k in solution)
            combination.append(i)
            if((best_solution is None ) or (len(combination)<len(best_solution))):
                best_solution=combination
    memo[key] =best_solution
    return best_solution

With this change, every time bestSum is called, memo will be a new empty dictionary if no other value is provided, so you won't have issues with data from previous calls affecting your results.


With that being said, here is an attempt to rewrite your solution to be slightly more Pythonic:

from typing import Optional


def best_sum(target: int,
             nums: list[int],
             memo: Optional[dict[int, int]] = None) -> Optional[list[int]]:
  """
  Function to find the shortest combination of integers that sums up to target. 
  Each number in the nums list can be used an unlimited number of times. 

  Args:
      target: The target sum.
      nums: List of integers available.
      memo: Memoization dictionary. Defaults to None.

  Returns:
      Optional[list[int]]: The shortest combination of integers from nums 
                            that sum up to target. If no combination is 
                            possible, return None.
  """
  if memo is None:
    memo = {}
  if target < 0:
    return None
  if target == 0:
    return []
  if target in memo:
    return memo[target]
  best_solution = None
  for num in nums:
    remainder = target - num
    remainder_combination = best_sum(remainder, nums, memo)
    if remainder_combination is not None:
      combination = remainder_combination + [num]
      if best_solution is None or len(combination) < len(best_solution):
        best_solution = combination
  memo[target] = best_solution
  return best_solution

print(best_sum(8, [3, 2]))  # [2, 3, 3]
print(best_sum(8, [4, 2]))  # [4, 4]
Sash Sinha
  • 18,743
  • 3
  • 23
  • 40
  • I spent a day trying to figure this out. THANK YOU SO MUCH !!!!!! – Non_CS_Dude Aug 06 '23 at 17:08
  • I have one more doubt- "Is it true for python only? In JavaScript the same code with the use of a default mutable argument seems to work fine" – Non_CS_Dude Aug 06 '23 at 17:10
  • The behavior of default arguments in Python is different from JavaScript. In JavaScript, parameters are always passed by value, and every time a function is called, a new execution context is created, so a new instance of the default argument object is created. This means that changes to the object inside the function do not persist across function calls, which is different from Python. – Sash Sinha Aug 06 '23 at 17:18