Project Euler Problem 31 (Coin Sum)
Clash Royale CLAN TAG#URR8PPP
.everyoneloves__top-leaderboard:empty,.everyoneloves__mid-leaderboard:empty margin-bottom:0;
up vote
1
down vote
favorite
I just finished Project Euler 31: Coin sums, which asks how many ways there are to make ã2 using British coins (1p, 2p, 5p, 10p, 20p, 50p, ã1, and ã2).
When I compared my code and the problem review's algorithms, I found that my code was faster than theirs. Both of them use dynamic programming, but for some unknown reasons, my code meets the recursive limits.
Project Euler Reference
def problem_31_dynamic_programming(money, coin_index):
count = 0
if coin_index <= 0:
return 1
m = money
if memoiz_list[m][coin_index] > 0:
return memoiz_list[m][coin_index]
while money >= 0:
count += problem_31_dynamic_programming(money, coin_index - 1)
money -= coin_list[coin_index]
memoiz_list[m][coin_index] = count
return count
My solution
import time
def problem_31(money, coin_index): #My solution (cannot solve big number such as 10000)
if coin_index < 0:
return 0
if coin_index == 0 or money == 0:
return 1
if memoiz_list[money][coin_index] > 0:
return memoiz_list[money][coin_index]
if coin_list[coin_index] > money:
return problem_31(money, coin_index - 1)
memoiz_list[money][coin_index] = problem_31(money, coin_index-1)+
problem_31(money-coin_list[coin_index],coin_index)
return memoiz_list[money][coin_index]
start = time.time()
coin_list = [1,2,5,10,20,50,100,200]
memoiz_list = [[0,0,0,0,0,0,0,0] for i in range(201)]
print(problem_31_dynamic_programming(200,7)) #Replace problem_31_dynamic_programming() with problem_31
elapsed = time.time() - start
print("Result found in %f seconds"%(elapsed))
python python-3.x programming-challenge dynamic-programming change-making-problem
add a comment |Â
up vote
1
down vote
favorite
I just finished Project Euler 31: Coin sums, which asks how many ways there are to make ã2 using British coins (1p, 2p, 5p, 10p, 20p, 50p, ã1, and ã2).
When I compared my code and the problem review's algorithms, I found that my code was faster than theirs. Both of them use dynamic programming, but for some unknown reasons, my code meets the recursive limits.
Project Euler Reference
def problem_31_dynamic_programming(money, coin_index):
count = 0
if coin_index <= 0:
return 1
m = money
if memoiz_list[m][coin_index] > 0:
return memoiz_list[m][coin_index]
while money >= 0:
count += problem_31_dynamic_programming(money, coin_index - 1)
money -= coin_list[coin_index]
memoiz_list[m][coin_index] = count
return count
My solution
import time
def problem_31(money, coin_index): #My solution (cannot solve big number such as 10000)
if coin_index < 0:
return 0
if coin_index == 0 or money == 0:
return 1
if memoiz_list[money][coin_index] > 0:
return memoiz_list[money][coin_index]
if coin_list[coin_index] > money:
return problem_31(money, coin_index - 1)
memoiz_list[money][coin_index] = problem_31(money, coin_index-1)+
problem_31(money-coin_list[coin_index],coin_index)
return memoiz_list[money][coin_index]
start = time.time()
coin_list = [1,2,5,10,20,50,100,200]
memoiz_list = [[0,0,0,0,0,0,0,0] for i in range(201)]
print(problem_31_dynamic_programming(200,7)) #Replace problem_31_dynamic_programming() with problem_31
elapsed = time.time() - start
print("Result found in %f seconds"%(elapsed))
python python-3.x programming-challenge dynamic-programming change-making-problem
add a comment |Â
up vote
1
down vote
favorite
up vote
1
down vote
favorite
I just finished Project Euler 31: Coin sums, which asks how many ways there are to make ã2 using British coins (1p, 2p, 5p, 10p, 20p, 50p, ã1, and ã2).
When I compared my code and the problem review's algorithms, I found that my code was faster than theirs. Both of them use dynamic programming, but for some unknown reasons, my code meets the recursive limits.
Project Euler Reference
def problem_31_dynamic_programming(money, coin_index):
count = 0
if coin_index <= 0:
return 1
m = money
if memoiz_list[m][coin_index] > 0:
return memoiz_list[m][coin_index]
while money >= 0:
count += problem_31_dynamic_programming(money, coin_index - 1)
money -= coin_list[coin_index]
memoiz_list[m][coin_index] = count
return count
My solution
import time
def problem_31(money, coin_index): #My solution (cannot solve big number such as 10000)
if coin_index < 0:
return 0
if coin_index == 0 or money == 0:
return 1
if memoiz_list[money][coin_index] > 0:
return memoiz_list[money][coin_index]
if coin_list[coin_index] > money:
return problem_31(money, coin_index - 1)
memoiz_list[money][coin_index] = problem_31(money, coin_index-1)+
problem_31(money-coin_list[coin_index],coin_index)
return memoiz_list[money][coin_index]
start = time.time()
coin_list = [1,2,5,10,20,50,100,200]
memoiz_list = [[0,0,0,0,0,0,0,0] for i in range(201)]
print(problem_31_dynamic_programming(200,7)) #Replace problem_31_dynamic_programming() with problem_31
elapsed = time.time() - start
print("Result found in %f seconds"%(elapsed))
python python-3.x programming-challenge dynamic-programming change-making-problem
I just finished Project Euler 31: Coin sums, which asks how many ways there are to make ã2 using British coins (1p, 2p, 5p, 10p, 20p, 50p, ã1, and ã2).
When I compared my code and the problem review's algorithms, I found that my code was faster than theirs. Both of them use dynamic programming, but for some unknown reasons, my code meets the recursive limits.
Project Euler Reference
def problem_31_dynamic_programming(money, coin_index):
count = 0
if coin_index <= 0:
return 1
m = money
if memoiz_list[m][coin_index] > 0:
return memoiz_list[m][coin_index]
while money >= 0:
count += problem_31_dynamic_programming(money, coin_index - 1)
money -= coin_list[coin_index]
memoiz_list[m][coin_index] = count
return count
My solution
import time
def problem_31(money, coin_index): #My solution (cannot solve big number such as 10000)
if coin_index < 0:
return 0
if coin_index == 0 or money == 0:
return 1
if memoiz_list[money][coin_index] > 0:
return memoiz_list[money][coin_index]
if coin_list[coin_index] > money:
return problem_31(money, coin_index - 1)
memoiz_list[money][coin_index] = problem_31(money, coin_index-1)+
problem_31(money-coin_list[coin_index],coin_index)
return memoiz_list[money][coin_index]
start = time.time()
coin_list = [1,2,5,10,20,50,100,200]
memoiz_list = [[0,0,0,0,0,0,0,0] for i in range(201)]
print(problem_31_dynamic_programming(200,7)) #Replace problem_31_dynamic_programming() with problem_31
elapsed = time.time() - start
print("Result found in %f seconds"%(elapsed))
python python-3.x programming-challenge dynamic-programming change-making-problem
edited Feb 9 at 21:39
200_success
123k14143401
123k14143401
asked Feb 9 at 3:59
Quang Truong
82
82
add a comment |Â
add a comment |Â
1 Answer
1
active
oldest
votes
up vote
1
down vote
accepted
Depth analysis
A bit of tweaking of your code leads to results about the maximum function call depth:
def problem_31_a(money, coin_index, depth=1):
global glob_depth
glob_depth = max(glob_depth, depth)
count = 0
if coin_index <= 0:
return 1
m = money
if memoiz_list[m][coin_index] > 0:
return memoiz_list[m][coin_index]
while money >= 0:
count += problem_31_a(money, coin_index - 1, depth=depth+1)
money -= coin_list[coin_index]
memoiz_list[m][coin_index] = count
return count
def problem_31_b(money, coin_index, depth=1):
global glob_depth
glob_depth = max(glob_depth, depth)
if coin_index < 0:
return 0
if coin_index == 0 or money == 0:
return 1
if memoiz_list[money][coin_index] > 0:
return memoiz_list[money][coin_index]
if coin_list[coin_index] > money:
return problem_31_b(money, coin_index - 1, depth=depth+1)
memoiz_list[money][coin_index] = problem_31_b(money, coin_index-1, depth=depth+1)+
problem_31_b(money-coin_list[coin_index],coin_index, depth=depth+1)
return memoiz_list[money][coin_index]
coin_list = [1,2,5,10,20,50,100,200]
for func in [problem_31_a, problem_31_b]:
glob_depth = 0
start = time.time()
memoiz_list = [[0,0,0,0,0,0,0,0] for i in range(201)]
print(func(200,7))
elapsed = time.time() - start
print("Result found in %f seconds - depth:%d" % (elapsed, glob_depth))
And indeed, we get:
73682
Result found in 0.003184 seconds - depth:8
73682
Result found in 0.000919 seconds - depth:107
Your code appears to be faster but also goes much deeper in the function calls. If you exceed the system limits, you could update sys.setrecursionlimit
. However, it could be a good idea to try to fix your code.
You could write a solution that doesn't perform any recursive calls: instead of trying to solve your problems by solving smaller and smaller problems and saving the solution as you go, you could simply update all the problems from the smallest to the biggest you need.
For instance, you could write:
def problem_31_c(money, unused):
nb_ways = [1] + [0] * money
for c in coin_list:
for v in range(money + 1 - c):
nb_ways[v + c] += nb_ways[v]
return nb_ways[-1]
Actual code review
For both functions, it could be a good idea to make it obvious that we have the following pattern:
if value_from_memoiz_list:
return value_from_memoiz_list
compute_value
store_value_in_memoiz_list
return value
In problem_31
for instance, we can see that a situation leads to a result being computed and returned without being store in the memoized list. Also, that case could be handled with less duplicated logic:
count = problem_31_b(money, coin_index-1)
coin_value = coin_list[coin_index]
if coin_value <= money:
count += problem_31_b(money-coin_value,coin_index)
Finally, your strategy reusing computed value assumes that 0 is a non-existing result. You could use None
for this. In your case it doesn't make a difference because no expensive computation leads to 0 but it makes the intent of your code clearer.
def problem_31_a(money, coin_index):
if coin_index <= 0:
return 1
money_rem = money
memo_value = memoiz_list[money][coin_index]
if memo_value is not None:
return memo_value
count = 0
while money_rem >= 0:
count += problem_31_a(money_rem, coin_index - 1)
money_rem -= coin_list[coin_index]
memoiz_list[money][coin_index] = count
return count
def problem_31_b(money, coin_index):
if coin_index < 0:
return 0
if coin_index == 0 or money == 0:
return 1
memo_value = memoiz_list[money][coin_index]
if memo_value is not None:
return memo_value
count = problem_31_b(money, coin_index-1)
coin_value = coin_list[coin_index]
if coin_value <= money:
count += problem_31_b(money-coin_value,coin_index)
memoiz_list[money][coin_index] = count
return count
Also, you could use a decorator to implement your memoization strategy.
Reusing a generic decorator from https://wiki.python.org/moin/PythonDecoratorLibrary#Memoize , you could write:
coin_list = [1,2,5,10,20,50,100,200]
class memoized(object):
'''Decorator. Caches a function's return value each time it is called.
If called later with the same arguments, the cached value is returned
(not reevaluated).
'''
def __init__(self, func):
self.func = func
self.memoiz_list = [[None]*len(coin_list) for i in range(201)]
def __call__(self, money, coin_index):
try:
memo_value = self.memoiz_list[money][coin_index]
if memo_value is not None:
return memo_value
except IndexError:
pass
ret = self.func(money, coin_index)
try:
self.memoiz_list[money][coin_index] = ret
except IndexError:
pass
return ret
def __repr__(self):
'''Return the function's docstring.'''
return self.func.__doc__
def __get__(self, obj, objtype):
'''Support instance methods.'''
return functools.partial(self.__call__, obj)
@memoized
def problem_31_a(money, coin_index):
if coin_index <= 0:
return 1
money_rem = money
count = 0
while money_rem >= 0:
count += problem_31_a(money_rem, coin_index - 1)
money_rem -= coin_list[coin_index]
return count
@memoized
def problem_31_b(money, coin_index):
if coin_index < 0:
return 0
if coin_index == 0 or money == 0:
return 1
count = problem_31_b(money, coin_index-1)
coin_value = coin_list[coin_index]
if coin_value <= money:
count += problem_31_b(money-coin_value,coin_index)
return count
memoized = functools.lru_cache(maxsize=None)
â Gareth Rees
Feb 10 at 16:27
add a comment |Â
1 Answer
1
active
oldest
votes
1 Answer
1
active
oldest
votes
active
oldest
votes
active
oldest
votes
up vote
1
down vote
accepted
Depth analysis
A bit of tweaking of your code leads to results about the maximum function call depth:
def problem_31_a(money, coin_index, depth=1):
global glob_depth
glob_depth = max(glob_depth, depth)
count = 0
if coin_index <= 0:
return 1
m = money
if memoiz_list[m][coin_index] > 0:
return memoiz_list[m][coin_index]
while money >= 0:
count += problem_31_a(money, coin_index - 1, depth=depth+1)
money -= coin_list[coin_index]
memoiz_list[m][coin_index] = count
return count
def problem_31_b(money, coin_index, depth=1):
global glob_depth
glob_depth = max(glob_depth, depth)
if coin_index < 0:
return 0
if coin_index == 0 or money == 0:
return 1
if memoiz_list[money][coin_index] > 0:
return memoiz_list[money][coin_index]
if coin_list[coin_index] > money:
return problem_31_b(money, coin_index - 1, depth=depth+1)
memoiz_list[money][coin_index] = problem_31_b(money, coin_index-1, depth=depth+1)+
problem_31_b(money-coin_list[coin_index],coin_index, depth=depth+1)
return memoiz_list[money][coin_index]
coin_list = [1,2,5,10,20,50,100,200]
for func in [problem_31_a, problem_31_b]:
glob_depth = 0
start = time.time()
memoiz_list = [[0,0,0,0,0,0,0,0] for i in range(201)]
print(func(200,7))
elapsed = time.time() - start
print("Result found in %f seconds - depth:%d" % (elapsed, glob_depth))
And indeed, we get:
73682
Result found in 0.003184 seconds - depth:8
73682
Result found in 0.000919 seconds - depth:107
Your code appears to be faster but also goes much deeper in the function calls. If you exceed the system limits, you could update sys.setrecursionlimit
. However, it could be a good idea to try to fix your code.
You could write a solution that doesn't perform any recursive calls: instead of trying to solve your problems by solving smaller and smaller problems and saving the solution as you go, you could simply update all the problems from the smallest to the biggest you need.
For instance, you could write:
def problem_31_c(money, unused):
nb_ways = [1] + [0] * money
for c in coin_list:
for v in range(money + 1 - c):
nb_ways[v + c] += nb_ways[v]
return nb_ways[-1]
Actual code review
For both functions, it could be a good idea to make it obvious that we have the following pattern:
if value_from_memoiz_list:
return value_from_memoiz_list
compute_value
store_value_in_memoiz_list
return value
In problem_31
for instance, we can see that a situation leads to a result being computed and returned without being store in the memoized list. Also, that case could be handled with less duplicated logic:
count = problem_31_b(money, coin_index-1)
coin_value = coin_list[coin_index]
if coin_value <= money:
count += problem_31_b(money-coin_value,coin_index)
Finally, your strategy reusing computed value assumes that 0 is a non-existing result. You could use None
for this. In your case it doesn't make a difference because no expensive computation leads to 0 but it makes the intent of your code clearer.
def problem_31_a(money, coin_index):
if coin_index <= 0:
return 1
money_rem = money
memo_value = memoiz_list[money][coin_index]
if memo_value is not None:
return memo_value
count = 0
while money_rem >= 0:
count += problem_31_a(money_rem, coin_index - 1)
money_rem -= coin_list[coin_index]
memoiz_list[money][coin_index] = count
return count
def problem_31_b(money, coin_index):
if coin_index < 0:
return 0
if coin_index == 0 or money == 0:
return 1
memo_value = memoiz_list[money][coin_index]
if memo_value is not None:
return memo_value
count = problem_31_b(money, coin_index-1)
coin_value = coin_list[coin_index]
if coin_value <= money:
count += problem_31_b(money-coin_value,coin_index)
memoiz_list[money][coin_index] = count
return count
Also, you could use a decorator to implement your memoization strategy.
Reusing a generic decorator from https://wiki.python.org/moin/PythonDecoratorLibrary#Memoize , you could write:
coin_list = [1,2,5,10,20,50,100,200]
class memoized(object):
'''Decorator. Caches a function's return value each time it is called.
If called later with the same arguments, the cached value is returned
(not reevaluated).
'''
def __init__(self, func):
self.func = func
self.memoiz_list = [[None]*len(coin_list) for i in range(201)]
def __call__(self, money, coin_index):
try:
memo_value = self.memoiz_list[money][coin_index]
if memo_value is not None:
return memo_value
except IndexError:
pass
ret = self.func(money, coin_index)
try:
self.memoiz_list[money][coin_index] = ret
except IndexError:
pass
return ret
def __repr__(self):
'''Return the function's docstring.'''
return self.func.__doc__
def __get__(self, obj, objtype):
'''Support instance methods.'''
return functools.partial(self.__call__, obj)
@memoized
def problem_31_a(money, coin_index):
if coin_index <= 0:
return 1
money_rem = money
count = 0
while money_rem >= 0:
count += problem_31_a(money_rem, coin_index - 1)
money_rem -= coin_list[coin_index]
return count
@memoized
def problem_31_b(money, coin_index):
if coin_index < 0:
return 0
if coin_index == 0 or money == 0:
return 1
count = problem_31_b(money, coin_index-1)
coin_value = coin_list[coin_index]
if coin_value <= money:
count += problem_31_b(money-coin_value,coin_index)
return count
memoized = functools.lru_cache(maxsize=None)
â Gareth Rees
Feb 10 at 16:27
add a comment |Â
up vote
1
down vote
accepted
Depth analysis
A bit of tweaking of your code leads to results about the maximum function call depth:
def problem_31_a(money, coin_index, depth=1):
global glob_depth
glob_depth = max(glob_depth, depth)
count = 0
if coin_index <= 0:
return 1
m = money
if memoiz_list[m][coin_index] > 0:
return memoiz_list[m][coin_index]
while money >= 0:
count += problem_31_a(money, coin_index - 1, depth=depth+1)
money -= coin_list[coin_index]
memoiz_list[m][coin_index] = count
return count
def problem_31_b(money, coin_index, depth=1):
global glob_depth
glob_depth = max(glob_depth, depth)
if coin_index < 0:
return 0
if coin_index == 0 or money == 0:
return 1
if memoiz_list[money][coin_index] > 0:
return memoiz_list[money][coin_index]
if coin_list[coin_index] > money:
return problem_31_b(money, coin_index - 1, depth=depth+1)
memoiz_list[money][coin_index] = problem_31_b(money, coin_index-1, depth=depth+1)+
problem_31_b(money-coin_list[coin_index],coin_index, depth=depth+1)
return memoiz_list[money][coin_index]
coin_list = [1,2,5,10,20,50,100,200]
for func in [problem_31_a, problem_31_b]:
glob_depth = 0
start = time.time()
memoiz_list = [[0,0,0,0,0,0,0,0] for i in range(201)]
print(func(200,7))
elapsed = time.time() - start
print("Result found in %f seconds - depth:%d" % (elapsed, glob_depth))
And indeed, we get:
73682
Result found in 0.003184 seconds - depth:8
73682
Result found in 0.000919 seconds - depth:107
Your code appears to be faster but also goes much deeper in the function calls. If you exceed the system limits, you could update sys.setrecursionlimit
. However, it could be a good idea to try to fix your code.
You could write a solution that doesn't perform any recursive calls: instead of trying to solve your problems by solving smaller and smaller problems and saving the solution as you go, you could simply update all the problems from the smallest to the biggest you need.
For instance, you could write:
def problem_31_c(money, unused):
nb_ways = [1] + [0] * money
for c in coin_list:
for v in range(money + 1 - c):
nb_ways[v + c] += nb_ways[v]
return nb_ways[-1]
Actual code review
For both functions, it could be a good idea to make it obvious that we have the following pattern:
if value_from_memoiz_list:
return value_from_memoiz_list
compute_value
store_value_in_memoiz_list
return value
In problem_31
for instance, we can see that a situation leads to a result being computed and returned without being store in the memoized list. Also, that case could be handled with less duplicated logic:
count = problem_31_b(money, coin_index-1)
coin_value = coin_list[coin_index]
if coin_value <= money:
count += problem_31_b(money-coin_value,coin_index)
Finally, your strategy reusing computed value assumes that 0 is a non-existing result. You could use None
for this. In your case it doesn't make a difference because no expensive computation leads to 0 but it makes the intent of your code clearer.
def problem_31_a(money, coin_index):
if coin_index <= 0:
return 1
money_rem = money
memo_value = memoiz_list[money][coin_index]
if memo_value is not None:
return memo_value
count = 0
while money_rem >= 0:
count += problem_31_a(money_rem, coin_index - 1)
money_rem -= coin_list[coin_index]
memoiz_list[money][coin_index] = count
return count
def problem_31_b(money, coin_index):
if coin_index < 0:
return 0
if coin_index == 0 or money == 0:
return 1
memo_value = memoiz_list[money][coin_index]
if memo_value is not None:
return memo_value
count = problem_31_b(money, coin_index-1)
coin_value = coin_list[coin_index]
if coin_value <= money:
count += problem_31_b(money-coin_value,coin_index)
memoiz_list[money][coin_index] = count
return count
Also, you could use a decorator to implement your memoization strategy.
Reusing a generic decorator from https://wiki.python.org/moin/PythonDecoratorLibrary#Memoize , you could write:
coin_list = [1,2,5,10,20,50,100,200]
class memoized(object):
'''Decorator. Caches a function's return value each time it is called.
If called later with the same arguments, the cached value is returned
(not reevaluated).
'''
def __init__(self, func):
self.func = func
self.memoiz_list = [[None]*len(coin_list) for i in range(201)]
def __call__(self, money, coin_index):
try:
memo_value = self.memoiz_list[money][coin_index]
if memo_value is not None:
return memo_value
except IndexError:
pass
ret = self.func(money, coin_index)
try:
self.memoiz_list[money][coin_index] = ret
except IndexError:
pass
return ret
def __repr__(self):
'''Return the function's docstring.'''
return self.func.__doc__
def __get__(self, obj, objtype):
'''Support instance methods.'''
return functools.partial(self.__call__, obj)
@memoized
def problem_31_a(money, coin_index):
if coin_index <= 0:
return 1
money_rem = money
count = 0
while money_rem >= 0:
count += problem_31_a(money_rem, coin_index - 1)
money_rem -= coin_list[coin_index]
return count
@memoized
def problem_31_b(money, coin_index):
if coin_index < 0:
return 0
if coin_index == 0 or money == 0:
return 1
count = problem_31_b(money, coin_index-1)
coin_value = coin_list[coin_index]
if coin_value <= money:
count += problem_31_b(money-coin_value,coin_index)
return count
memoized = functools.lru_cache(maxsize=None)
â Gareth Rees
Feb 10 at 16:27
add a comment |Â
up vote
1
down vote
accepted
up vote
1
down vote
accepted
Depth analysis
A bit of tweaking of your code leads to results about the maximum function call depth:
def problem_31_a(money, coin_index, depth=1):
global glob_depth
glob_depth = max(glob_depth, depth)
count = 0
if coin_index <= 0:
return 1
m = money
if memoiz_list[m][coin_index] > 0:
return memoiz_list[m][coin_index]
while money >= 0:
count += problem_31_a(money, coin_index - 1, depth=depth+1)
money -= coin_list[coin_index]
memoiz_list[m][coin_index] = count
return count
def problem_31_b(money, coin_index, depth=1):
global glob_depth
glob_depth = max(glob_depth, depth)
if coin_index < 0:
return 0
if coin_index == 0 or money == 0:
return 1
if memoiz_list[money][coin_index] > 0:
return memoiz_list[money][coin_index]
if coin_list[coin_index] > money:
return problem_31_b(money, coin_index - 1, depth=depth+1)
memoiz_list[money][coin_index] = problem_31_b(money, coin_index-1, depth=depth+1)+
problem_31_b(money-coin_list[coin_index],coin_index, depth=depth+1)
return memoiz_list[money][coin_index]
coin_list = [1,2,5,10,20,50,100,200]
for func in [problem_31_a, problem_31_b]:
glob_depth = 0
start = time.time()
memoiz_list = [[0,0,0,0,0,0,0,0] for i in range(201)]
print(func(200,7))
elapsed = time.time() - start
print("Result found in %f seconds - depth:%d" % (elapsed, glob_depth))
And indeed, we get:
73682
Result found in 0.003184 seconds - depth:8
73682
Result found in 0.000919 seconds - depth:107
Your code appears to be faster but also goes much deeper in the function calls. If you exceed the system limits, you could update sys.setrecursionlimit
. However, it could be a good idea to try to fix your code.
You could write a solution that doesn't perform any recursive calls: instead of trying to solve your problems by solving smaller and smaller problems and saving the solution as you go, you could simply update all the problems from the smallest to the biggest you need.
For instance, you could write:
def problem_31_c(money, unused):
nb_ways = [1] + [0] * money
for c in coin_list:
for v in range(money + 1 - c):
nb_ways[v + c] += nb_ways[v]
return nb_ways[-1]
Actual code review
For both functions, it could be a good idea to make it obvious that we have the following pattern:
if value_from_memoiz_list:
return value_from_memoiz_list
compute_value
store_value_in_memoiz_list
return value
In problem_31
for instance, we can see that a situation leads to a result being computed and returned without being store in the memoized list. Also, that case could be handled with less duplicated logic:
count = problem_31_b(money, coin_index-1)
coin_value = coin_list[coin_index]
if coin_value <= money:
count += problem_31_b(money-coin_value,coin_index)
Finally, your strategy reusing computed value assumes that 0 is a non-existing result. You could use None
for this. In your case it doesn't make a difference because no expensive computation leads to 0 but it makes the intent of your code clearer.
def problem_31_a(money, coin_index):
if coin_index <= 0:
return 1
money_rem = money
memo_value = memoiz_list[money][coin_index]
if memo_value is not None:
return memo_value
count = 0
while money_rem >= 0:
count += problem_31_a(money_rem, coin_index - 1)
money_rem -= coin_list[coin_index]
memoiz_list[money][coin_index] = count
return count
def problem_31_b(money, coin_index):
if coin_index < 0:
return 0
if coin_index == 0 or money == 0:
return 1
memo_value = memoiz_list[money][coin_index]
if memo_value is not None:
return memo_value
count = problem_31_b(money, coin_index-1)
coin_value = coin_list[coin_index]
if coin_value <= money:
count += problem_31_b(money-coin_value,coin_index)
memoiz_list[money][coin_index] = count
return count
Also, you could use a decorator to implement your memoization strategy.
Reusing a generic decorator from https://wiki.python.org/moin/PythonDecoratorLibrary#Memoize , you could write:
coin_list = [1,2,5,10,20,50,100,200]
class memoized(object):
'''Decorator. Caches a function's return value each time it is called.
If called later with the same arguments, the cached value is returned
(not reevaluated).
'''
def __init__(self, func):
self.func = func
self.memoiz_list = [[None]*len(coin_list) for i in range(201)]
def __call__(self, money, coin_index):
try:
memo_value = self.memoiz_list[money][coin_index]
if memo_value is not None:
return memo_value
except IndexError:
pass
ret = self.func(money, coin_index)
try:
self.memoiz_list[money][coin_index] = ret
except IndexError:
pass
return ret
def __repr__(self):
'''Return the function's docstring.'''
return self.func.__doc__
def __get__(self, obj, objtype):
'''Support instance methods.'''
return functools.partial(self.__call__, obj)
@memoized
def problem_31_a(money, coin_index):
if coin_index <= 0:
return 1
money_rem = money
count = 0
while money_rem >= 0:
count += problem_31_a(money_rem, coin_index - 1)
money_rem -= coin_list[coin_index]
return count
@memoized
def problem_31_b(money, coin_index):
if coin_index < 0:
return 0
if coin_index == 0 or money == 0:
return 1
count = problem_31_b(money, coin_index-1)
coin_value = coin_list[coin_index]
if coin_value <= money:
count += problem_31_b(money-coin_value,coin_index)
return count
Depth analysis
A bit of tweaking of your code leads to results about the maximum function call depth:
def problem_31_a(money, coin_index, depth=1):
global glob_depth
glob_depth = max(glob_depth, depth)
count = 0
if coin_index <= 0:
return 1
m = money
if memoiz_list[m][coin_index] > 0:
return memoiz_list[m][coin_index]
while money >= 0:
count += problem_31_a(money, coin_index - 1, depth=depth+1)
money -= coin_list[coin_index]
memoiz_list[m][coin_index] = count
return count
def problem_31_b(money, coin_index, depth=1):
global glob_depth
glob_depth = max(glob_depth, depth)
if coin_index < 0:
return 0
if coin_index == 0 or money == 0:
return 1
if memoiz_list[money][coin_index] > 0:
return memoiz_list[money][coin_index]
if coin_list[coin_index] > money:
return problem_31_b(money, coin_index - 1, depth=depth+1)
memoiz_list[money][coin_index] = problem_31_b(money, coin_index-1, depth=depth+1)+
problem_31_b(money-coin_list[coin_index],coin_index, depth=depth+1)
return memoiz_list[money][coin_index]
coin_list = [1,2,5,10,20,50,100,200]
for func in [problem_31_a, problem_31_b]:
glob_depth = 0
start = time.time()
memoiz_list = [[0,0,0,0,0,0,0,0] for i in range(201)]
print(func(200,7))
elapsed = time.time() - start
print("Result found in %f seconds - depth:%d" % (elapsed, glob_depth))
And indeed, we get:
73682
Result found in 0.003184 seconds - depth:8
73682
Result found in 0.000919 seconds - depth:107
Your code appears to be faster but also goes much deeper in the function calls. If you exceed the system limits, you could update sys.setrecursionlimit
. However, it could be a good idea to try to fix your code.
You could write a solution that doesn't perform any recursive calls: instead of trying to solve your problems by solving smaller and smaller problems and saving the solution as you go, you could simply update all the problems from the smallest to the biggest you need.
For instance, you could write:
def problem_31_c(money, unused):
nb_ways = [1] + [0] * money
for c in coin_list:
for v in range(money + 1 - c):
nb_ways[v + c] += nb_ways[v]
return nb_ways[-1]
Actual code review
For both functions, it could be a good idea to make it obvious that we have the following pattern:
if value_from_memoiz_list:
return value_from_memoiz_list
compute_value
store_value_in_memoiz_list
return value
In problem_31
for instance, we can see that a situation leads to a result being computed and returned without being store in the memoized list. Also, that case could be handled with less duplicated logic:
count = problem_31_b(money, coin_index-1)
coin_value = coin_list[coin_index]
if coin_value <= money:
count += problem_31_b(money-coin_value,coin_index)
Finally, your strategy reusing computed value assumes that 0 is a non-existing result. You could use None
for this. In your case it doesn't make a difference because no expensive computation leads to 0 but it makes the intent of your code clearer.
def problem_31_a(money, coin_index):
if coin_index <= 0:
return 1
money_rem = money
memo_value = memoiz_list[money][coin_index]
if memo_value is not None:
return memo_value
count = 0
while money_rem >= 0:
count += problem_31_a(money_rem, coin_index - 1)
money_rem -= coin_list[coin_index]
memoiz_list[money][coin_index] = count
return count
def problem_31_b(money, coin_index):
if coin_index < 0:
return 0
if coin_index == 0 or money == 0:
return 1
memo_value = memoiz_list[money][coin_index]
if memo_value is not None:
return memo_value
count = problem_31_b(money, coin_index-1)
coin_value = coin_list[coin_index]
if coin_value <= money:
count += problem_31_b(money-coin_value,coin_index)
memoiz_list[money][coin_index] = count
return count
Also, you could use a decorator to implement your memoization strategy.
Reusing a generic decorator from https://wiki.python.org/moin/PythonDecoratorLibrary#Memoize , you could write:
coin_list = [1,2,5,10,20,50,100,200]
class memoized(object):
'''Decorator. Caches a function's return value each time it is called.
If called later with the same arguments, the cached value is returned
(not reevaluated).
'''
def __init__(self, func):
self.func = func
self.memoiz_list = [[None]*len(coin_list) for i in range(201)]
def __call__(self, money, coin_index):
try:
memo_value = self.memoiz_list[money][coin_index]
if memo_value is not None:
return memo_value
except IndexError:
pass
ret = self.func(money, coin_index)
try:
self.memoiz_list[money][coin_index] = ret
except IndexError:
pass
return ret
def __repr__(self):
'''Return the function's docstring.'''
return self.func.__doc__
def __get__(self, obj, objtype):
'''Support instance methods.'''
return functools.partial(self.__call__, obj)
@memoized
def problem_31_a(money, coin_index):
if coin_index <= 0:
return 1
money_rem = money
count = 0
while money_rem >= 0:
count += problem_31_a(money_rem, coin_index - 1)
money_rem -= coin_list[coin_index]
return count
@memoized
def problem_31_b(money, coin_index):
if coin_index < 0:
return 0
if coin_index == 0 or money == 0:
return 1
count = problem_31_b(money, coin_index-1)
coin_value = coin_list[coin_index]
if coin_value <= money:
count += problem_31_b(money-coin_value,coin_index)
return count
edited Feb 10 at 9:43
answered Feb 9 at 14:59
Josay
23.8k13580
23.8k13580
memoized = functools.lru_cache(maxsize=None)
â Gareth Rees
Feb 10 at 16:27
add a comment |Â
memoized = functools.lru_cache(maxsize=None)
â Gareth Rees
Feb 10 at 16:27
memoized = functools.lru_cache(maxsize=None)
â Gareth Rees
Feb 10 at 16:27
memoized = functools.lru_cache(maxsize=None)
â Gareth Rees
Feb 10 at 16:27
add a comment |Â
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
StackExchange.ready(
function ()
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f187150%2fproject-euler-problem-31-coin-sum%23new-answer', 'question_page');
);
Post as a guest
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password