From e33f43771be026c18d1a69e01a59ab2e1dee5ece Mon Sep 17 00:00:00 2001 From: nickzerjeski Date: Sat, 11 Apr 2026 16:09:36 +0200 Subject: [PATCH 1/4] Refactor memoized knapsack to remove global state --- dynamic_programming/knapsack.py | 69 +++++++++++++++++++++++---------- 1 file changed, 48 insertions(+), 21 deletions(-) diff --git a/dynamic_programming/knapsack.py b/dynamic_programming/knapsack.py index 28c5b19dbe36..ccb76810c4c3 100644 --- a/dynamic_programming/knapsack.py +++ b/dynamic_programming/knapsack.py @@ -6,27 +6,53 @@ using dynamic programming. """ +from __future__ import annotations -def mf_knapsack(i, wt, val, j): +from functools import lru_cache + + +def mf_knapsack(i: int, wt: list[int], val: list[int], j: int) -> int: """ - This code involves the concept of memory functions. Here we solve the subproblems - which are needed unlike the below example - F is a 2D array with ``-1`` s filled up + Return the optimal value for the 0/1 knapsack problem using memoization. + + This implementation caches subproblems with ``functools.lru_cache`` and avoids + global mutable state. + + >>> mf_knapsack(4, [4, 3, 2, 3], [3, 2, 4, 4], 6) + 8 + >>> mf_knapsack(3, [10, 20, 30], [60, 100, 120], 50) + 220 + >>> mf_knapsack(0, [1], [10], 50) + 0 """ - global f # a global dp table for knapsack - if f[i][j] < 0: - if j < wt[i - 1]: - val = mf_knapsack(i - 1, wt, val, j) - else: - val = max( - mf_knapsack(i - 1, wt, val, j), - mf_knapsack(i - 1, wt, val, j - wt[i - 1]) + val[i - 1], - ) - f[i][j] = val - return f[i][j] + if i < 0: + raise ValueError("The number of items to consider cannot be negative.") + if j < 0: + raise ValueError("The knapsack capacity cannot be negative.") + if len(wt) != len(val): + raise ValueError("The number of weights must match the number of values.") + if i > len(wt): + raise ValueError("The number of items to consider cannot exceed input length.") + + weights = tuple(wt) + values = tuple(val) + + @lru_cache(maxsize=None) + def solve(item_count: int, capacity: int) -> int: + if item_count == 0 or capacity == 0: + return 0 + if weights[item_count - 1] > capacity: + return solve(item_count - 1, capacity) + return max( + solve(item_count - 1, capacity), + solve(item_count - 1, capacity - weights[item_count - 1]) + + values[item_count - 1], + ) + + return solve(i, j) -def knapsack(w, wt, val, n): +def knapsack(w: int, wt: list[int], val: list[int], n: int) -> tuple[int, list[list[int]]]: dp = [[0] * (w + 1) for _ in range(n + 1)] for i in range(1, n + 1): @@ -36,10 +62,10 @@ def knapsack(w, wt, val, n): else: dp[i][w_] = dp[i - 1][w_] - return dp[n][w_], dp + return dp[n][w], dp -def knapsack_with_example_solution(w: int, wt: list, val: list): +def knapsack_with_example_solution(w: int, wt: list, val: list) -> tuple[int, set[int]]: """ Solves the integer weights knapsack problem returns one of the several possible optimal subsets. @@ -100,7 +126,9 @@ def knapsack_with_example_solution(w: int, wt: list, val: list): return optimal_val, example_optional_set -def _construct_solution(dp: list, wt: list, i: int, j: int, optimal_set: set): +def _construct_solution( + dp: list[list[int]], wt: list[int], i: int, j: int, optimal_set: set[int] +) -> None: """ Recursively reconstructs one of the optimal subsets given 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): wt = [4, 3, 2, 3] n = 4 w = 6 - f = [[0] * (w + 1)] + [[0] + [-1] * (w + 1) for _ in range(n + 1)] optimal_solution, _ = knapsack(w, wt, val, n) print(optimal_solution) - print(mf_knapsack(n, wt, val, w)) # switched the n and w + print(mf_knapsack(n, wt, val, w)) # testing the dynamic programming problem with example # the optimal subset for the above example are items 3 and 4 From 13522499268aaf6f8126a9b1b82a7a2c7e175cc9 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 11 Apr 2026 14:12:50 +0000 Subject: [PATCH 2/4] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- dynamic_programming/knapsack.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dynamic_programming/knapsack.py b/dynamic_programming/knapsack.py index ccb76810c4c3..11d090fe4d3f 100644 --- a/dynamic_programming/knapsack.py +++ b/dynamic_programming/knapsack.py @@ -52,7 +52,9 @@ def solve(item_count: int, capacity: int) -> int: return solve(i, j) -def knapsack(w: int, wt: list[int], val: list[int], n: int) -> tuple[int, list[list[int]]]: +def knapsack( + w: int, wt: list[int], val: list[int], n: int +) -> tuple[int, list[list[int]]]: dp = [[0] * (w + 1) for _ in range(n + 1)] for i in range(1, n + 1): From a3fc426a94beeb7f2805ba7605a429f6dcfac82b Mon Sep 17 00:00:00 2001 From: nickzerjeski Date: Sat, 11 Apr 2026 16:49:29 +0200 Subject: [PATCH 3/4] Align knapsack type hints with sequence inputs --- dynamic_programming/knapsack.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/dynamic_programming/knapsack.py b/dynamic_programming/knapsack.py index 11d090fe4d3f..723013860a66 100644 --- a/dynamic_programming/knapsack.py +++ b/dynamic_programming/knapsack.py @@ -8,10 +8,11 @@ from __future__ import annotations +from collections.abc import Sequence from functools import lru_cache -def mf_knapsack(i: int, wt: list[int], val: list[int], j: int) -> int: +def mf_knapsack(i: int, wt: Sequence[int], val: Sequence[int], j: int) -> int: """ Return the optimal value for the 0/1 knapsack problem using memoization. @@ -53,7 +54,7 @@ def solve(item_count: int, capacity: int) -> int: def knapsack( - w: int, wt: list[int], val: list[int], n: int + w: int, wt: Sequence[int], val: Sequence[int], n: int ) -> tuple[int, list[list[int]]]: dp = [[0] * (w + 1) for _ in range(n + 1)] @@ -67,7 +68,9 @@ def knapsack( return dp[n][w], dp -def knapsack_with_example_solution(w: int, wt: list, val: list) -> tuple[int, set[int]]: +def knapsack_with_example_solution( + w: int, wt: Sequence[int], val: Sequence[int] +) -> tuple[int, set[int]]: """ Solves the integer weights knapsack problem returns one of the several possible optimal subsets. @@ -101,9 +104,14 @@ def knapsack_with_example_solution(w: int, wt: list, val: list) -> tuple[int, se ValueError: The number of weights must be the same as the number of values. But got 4 weights and 3 values """ - if not (isinstance(wt, (list, tuple)) and isinstance(val, (list, tuple))): + if not ( + isinstance(wt, Sequence) + and not isinstance(wt, (str, bytes)) + and isinstance(val, Sequence) + and not isinstance(val, (str, bytes)) + ): raise ValueError( - "Both the weights and values vectors must be either lists or tuples" + "Both the weights and values vectors must be non-string sequences" ) num_items = len(wt) @@ -129,7 +137,7 @@ def knapsack_with_example_solution(w: int, wt: list, val: list) -> tuple[int, se def _construct_solution( - dp: list[list[int]], wt: list[int], i: int, j: int, optimal_set: set[int] + dp: list[list[int]], wt: Sequence[int], i: int, j: int, optimal_set: set[int] ) -> None: """ Recursively reconstructs one of the optimal subsets given From db897b71b3adda96047110ff54e99ebcf5081650 Mon Sep 17 00:00:00 2001 From: nickzerjeski Date: Sat, 11 Apr 2026 16:55:56 +0200 Subject: [PATCH 4/4] Use functools.cache to satisfy ruff UP033 --- dynamic_programming/knapsack.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dynamic_programming/knapsack.py b/dynamic_programming/knapsack.py index 723013860a66..80f4e9295b66 100644 --- a/dynamic_programming/knapsack.py +++ b/dynamic_programming/knapsack.py @@ -9,14 +9,14 @@ from __future__ import annotations from collections.abc import Sequence -from functools import lru_cache +from functools import cache def mf_knapsack(i: int, wt: Sequence[int], val: Sequence[int], j: int) -> int: """ Return the optimal value for the 0/1 knapsack problem using memoization. - This implementation caches subproblems with ``functools.lru_cache`` and avoids + This implementation caches subproblems with ``functools.cache`` and avoids global mutable state. >>> mf_knapsack(4, [4, 3, 2, 3], [3, 2, 4, 4], 6) @@ -38,7 +38,7 @@ def mf_knapsack(i: int, wt: Sequence[int], val: Sequence[int], j: int) -> int: weights = tuple(wt) values = tuple(val) - @lru_cache(maxsize=None) + @cache def solve(item_count: int, capacity: int) -> int: if item_count == 0 or capacity == 0: return 0