Skip to content

Commit e33f437

Browse files
committed
Refactor memoized knapsack to remove global state
1 parent fc2f947 commit e33f437

1 file changed

Lines changed: 48 additions & 21 deletions

File tree

dynamic_programming/knapsack.py

Lines changed: 48 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,27 +6,53 @@
66
using dynamic programming.
77
"""
88

9+
from __future__ import annotations
910

10-
def mf_knapsack(i, wt, val, j):
11+
from functools import lru_cache
12+
13+
14+
def mf_knapsack(i: int, wt: list[int], val: list[int], j: int) -> int:
1115
"""
12-
This code involves the concept of memory functions. Here we solve the subproblems
13-
which are needed unlike the below example
14-
F is a 2D array with ``-1`` s filled up
16+
Return the optimal value for the 0/1 knapsack problem using memoization.
17+
18+
This implementation caches subproblems with ``functools.lru_cache`` and avoids
19+
global mutable state.
20+
21+
>>> mf_knapsack(4, [4, 3, 2, 3], [3, 2, 4, 4], 6)
22+
8
23+
>>> mf_knapsack(3, [10, 20, 30], [60, 100, 120], 50)
24+
220
25+
>>> mf_knapsack(0, [1], [10], 50)
26+
0
1527
"""
16-
global f # a global dp table for knapsack
17-
if f[i][j] < 0:
18-
if j < wt[i - 1]:
19-
val = mf_knapsack(i - 1, wt, val, j)
20-
else:
21-
val = max(
22-
mf_knapsack(i - 1, wt, val, j),
23-
mf_knapsack(i - 1, wt, val, j - wt[i - 1]) + val[i - 1],
24-
)
25-
f[i][j] = val
26-
return f[i][j]
28+
if i < 0:
29+
raise ValueError("The number of items to consider cannot be negative.")
30+
if j < 0:
31+
raise ValueError("The knapsack capacity cannot be negative.")
32+
if len(wt) != len(val):
33+
raise ValueError("The number of weights must match the number of values.")
34+
if i > len(wt):
35+
raise ValueError("The number of items to consider cannot exceed input length.")
36+
37+
weights = tuple(wt)
38+
values = tuple(val)
39+
40+
@lru_cache(maxsize=None)
41+
def solve(item_count: int, capacity: int) -> int:
42+
if item_count == 0 or capacity == 0:
43+
return 0
44+
if weights[item_count - 1] > capacity:
45+
return solve(item_count - 1, capacity)
46+
return max(
47+
solve(item_count - 1, capacity),
48+
solve(item_count - 1, capacity - weights[item_count - 1])
49+
+ values[item_count - 1],
50+
)
51+
52+
return solve(i, j)
2753

2854

29-
def knapsack(w, wt, val, n):
55+
def knapsack(w: int, wt: list[int], val: list[int], n: int) -> tuple[int, list[list[int]]]:
3056
dp = [[0] * (w + 1) for _ in range(n + 1)]
3157

3258
for i in range(1, n + 1):
@@ -36,10 +62,10 @@ def knapsack(w, wt, val, n):
3662
else:
3763
dp[i][w_] = dp[i - 1][w_]
3864

39-
return dp[n][w_], dp
65+
return dp[n][w], dp
4066

4167

42-
def knapsack_with_example_solution(w: int, wt: list, val: list):
68+
def knapsack_with_example_solution(w: int, wt: list, val: list) -> tuple[int, set[int]]:
4369
"""
4470
Solves the integer weights knapsack problem returns one of
4571
the several possible optimal subsets.
@@ -100,7 +126,9 @@ def knapsack_with_example_solution(w: int, wt: list, val: list):
100126
return optimal_val, example_optional_set
101127

102128

103-
def _construct_solution(dp: list, wt: list, i: int, j: int, optimal_set: set):
129+
def _construct_solution(
130+
dp: list[list[int]], wt: list[int], i: int, j: int, optimal_set: set[int]
131+
) -> None:
104132
"""
105133
Recursively reconstructs one of the optimal subsets given
106134
a filled DP table and the vector of weights
@@ -139,10 +167,9 @@ def _construct_solution(dp: list, wt: list, i: int, j: int, optimal_set: set):
139167
wt = [4, 3, 2, 3]
140168
n = 4
141169
w = 6
142-
f = [[0] * (w + 1)] + [[0] + [-1] * (w + 1) for _ in range(n + 1)]
143170
optimal_solution, _ = knapsack(w, wt, val, n)
144171
print(optimal_solution)
145-
print(mf_knapsack(n, wt, val, w)) # switched the n and w
172+
print(mf_knapsack(n, wt, val, w))
146173

147174
# testing the dynamic programming problem with example
148175
# the optimal subset for the above example are items 3 and 4

0 commit comments

Comments
 (0)