diff --git a/dynamic_programming/knapsack.py b/dynamic_programming/knapsack.py index 28c5b19dbe36..80f4e9295b66 100644 --- a/dynamic_programming/knapsack.py +++ b/dynamic_programming/knapsack.py @@ -6,27 +6,56 @@ using dynamic programming. """ +from __future__ import annotations -def mf_knapsack(i, wt, val, j): +from collections.abc import Sequence +from functools import cache + + +def mf_knapsack(i: int, wt: Sequence[int], val: Sequence[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.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) + + @cache + 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: Sequence[int], val: Sequence[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 +65,12 @@ 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: Sequence[int], val: Sequence[int] +) -> tuple[int, set[int]]: """ Solves the integer weights knapsack problem returns one of the several possible optimal subsets. @@ -73,9 +104,14 @@ def knapsack_with_example_solution(w: int, wt: list, val: list): 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) @@ -100,7 +136,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: Sequence[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 +177,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