I'm writing a Python program to calculate the maximum value of a polynomial $p * (1 + (d * (1 + (o * (1 + g))))$, subject to the constraints that $p$, $d$, $o$ and $g$ are all positive integers, and $p + 3*d + 6*o + 15*g \le s$, where $s$ is a user-defined constant. Right now, the program uses dynamic programming to calculate the optimal value of $(o * (1 + g))$ and $(d * (1 + (o * (1 + g)))$, given each possible value of p. I've gotten suggestions for improving the performance of the code at Code Review, but I'm wondering if there's some mathematical way of optimizing my algorithm.
Code:
'''
This program calculates the optimal number of game upgrades to purchase
given a certain number of points to spend on upgrades
Upgrades cost:
- pennies: 1
- dimes: 3
- dollars: 6
- gold: 15
The upgrade bonus can be calculated as:
pennies + pennies * dimes + pennies * dimes * dollars
+ pennies * dimes * dollars * gold
or alternatively:
pennies * (1 + dimes * (1 + dollars * (1 + gold)))
'''
from functools import cache
import time
'''
Input: points is the number of points to spend on pennies, dimes, dollars, and gold combined
Output: returns a nested tuple containing
((pennies, dimes, dollars, gold), total bonus)
'''
def get_max_score(points):
last_max_score = -1
last_max_combo = (0, 0, 0, 0)
# we decrement by 3 instead of 1, because the cost of all
# upgrades other than pennies is a multiple of 3.
# Therefore, the number of upgrades other than pennies that
# we can buy only changes every 3 pennies.
for pennies in range(points, -1, -3):
points_left = points - pennies
dimes, dollars, gold = get_max_dimes(points_left)
score = pennies * (1 + dimes * (1 + dollars * (1 + gold)))
if score >= last_max_score:
last_max_combo = (pennies, dimes, dollars, gold)
last_max_score = score
else:
# It seems like this function has one global maximum
# although I haven't proved this
pass # break
return last_max_combo, last_max_score
'''
Input: points is the number of points to spend on dimes, dollars, and gold combined
Output: returns a tuple containing
(dimes, dollars, gold)
'''
@cache
def get_max_dimes(points):
# the cost of dimes, dollars, and gold are all divisible by 3
# hence, the max number of coins with x points will be the same
# as the max number of coins with y points, where y is x rounded
# to the nearest multiple of 3
point_remainder = points % 3
if point_remainder != 0:
return get_max_dimes(points - point_remainder)
max_dimes = points // 3
last_max_score = -1
last_max_combo = (0, 0, 0)
for dimes in range(max_dimes, -1, -1):
points_left = points - dimes * 3
dollars, gold = get_max_dollars(points_left)
partial_score = dimes * (1 + dollars * (1 + gold))
if partial_score >= last_max_score:
last_max_combo = (dimes, dollars, gold)
last_max_score = partial_score
return last_max_combo
'''
Input: points is the number of points to spend on dollars and gold combined
Output: returns a tuple containing
(dollars, gold)
'''
@cache
def get_max_dollars(points):
max_dollars = points // 6
last_max_score = -1
last_max_combo = (0, 0)
for dollars in range(max_dollars, -1, -1):
points_left = points - dollars * 6
gold = points_left // 15
partial_score = dollars * (1 + gold)
if partial_score >= last_max_score:
last_max_combo = (dollars, gold)
last_max_score = partial_score
return last_max_combo
while True:
start_points = int(input("Start points: "))
end_points = input("End points (leave blank for single value): ")
if end_points == '':
end_points = start_points
end_points = int(end_points)
if end_points < start_points:
start_points, end_points = end_points, start_points
print()
time_start = time.perf_counter()
for y in range(end_points, start_points - 1, -1):
combo = get_max_score(y)
print(f"score: {y}, {combo}")
time_end = time.perf_counter()
print(f"\nComputation took {time_end - time_start:.5f}s")
if input("\nRun again? [y]/[n] ") != "y":
break
print()