From bc25a22b9b43f30006703666c4cdf5304e163fa3 Mon Sep 17 00:00:00 2001 From: nickzerjeski Date: Sat, 11 Apr 2026 16:16:03 +0200 Subject: [PATCH 1/3] Add knapsack variant counting optimal subsets --- knapsack/knapsack.py | 57 ++++++++++++++++++++++++++++++++- knapsack/tests/test_knapsack.py | 18 +++++++++++ 2 files changed, 74 insertions(+), 1 deletion(-) diff --git a/knapsack/knapsack.py b/knapsack/knapsack.py index 0648773c919f..dfcfd5dd53cb 100644 --- a/knapsack/knapsack.py +++ b/knapsack/knapsack.py @@ -12,7 +12,7 @@ def knapsack( weights: list[int], values: list[int], counter: int, - allow_repetition=False, + allow_repetition: bool = False, ) -> int: """ Returns the maximum value that can be put in a knapsack of a capacity cap, @@ -62,6 +62,61 @@ def knapsack_recur(capacity: int, counter: int) -> int: return knapsack_recur(capacity, counter) +def knapsack_with_count( + capacity: int, + weights: list[int], + values: list[int], + counter: int, + allow_repetition: bool = False, +) -> tuple[int, int]: + """ + Return both the maximum knapsack value and the number of optimal subsets. + + The return value is ``(max_value, number_of_optimal_subsets)``. + If multiple choices produce the same maximum value, their counts are added. + + >>> cap = 50 + >>> val = [60, 100, 120] + >>> w = [10, 20, 30] + >>> c = len(val) + >>> knapsack_with_count(cap, w, val, c) + (220, 1) + >>> knapsack_with_count(cap, w, val, c, True) + (300, 1) + >>> knapsack_with_count(3, [1, 2, 3], [1, 2, 3], 3) + (3, 2) + >>> knapsack_with_count(2, [1, 2], [1, 2], 2, True) + (2, 2) + """ + + @lru_cache + def knapsack_recur(remaining_capacity: int, item_count: int) -> tuple[int, int]: + # Base Case: one empty subset yields value 0. + if item_count == 0 or remaining_capacity == 0: + return 0, 1 + + if weights[item_count - 1] > remaining_capacity: + return knapsack_recur(remaining_capacity, item_count - 1) + + left_capacity = remaining_capacity - weights[item_count - 1] + included_value, included_count = knapsack_recur( + left_capacity, item_count if allow_repetition else item_count - 1 + ) + included_value += values[item_count - 1] + + excluded_value, excluded_count = knapsack_recur( + remaining_capacity, item_count - 1 + ) + + if included_value > excluded_value: + return included_value, included_count + if excluded_value > included_value: + return excluded_value, excluded_count + return included_value, included_count + excluded_count + + return knapsack_recur(capacity, counter) + + if __name__ == "__main__": import doctest diff --git a/knapsack/tests/test_knapsack.py b/knapsack/tests/test_knapsack.py index 80378aae4579..3c894a8e6fc1 100644 --- a/knapsack/tests/test_knapsack.py +++ b/knapsack/tests/test_knapsack.py @@ -58,6 +58,24 @@ def test_knapsack_repetition(self): c = len(val) assert k.knapsack(cap, w, val, c, True) == 300 + def test_knapsack_with_count(self): + """ + test for maximum value and number of optimal subsets + """ + cap = 50 + val = [60, 100, 120] + w = [10, 20, 30] + c = len(val) + assert k.knapsack_with_count(cap, w, val, c) == (220, 1) + assert k.knapsack_with_count(cap, w, val, c, True) == (300, 1) + + def test_knapsack_with_count_ties(self): + """ + test tie handling for counting optimal subsets + """ + assert k.knapsack_with_count(3, [1, 2, 3], [1, 2, 3], 3) == (3, 2) + assert k.knapsack_with_count(2, [1, 2], [1, 2], 2, True) == (2, 2) + if __name__ == "__main__": unittest.main() From 171481ea65ee75b5a6c027603752ebda5a533590 Mon Sep 17 00:00:00 2001 From: nickzerjeski Date: Sat, 11 Apr 2026 16:50:03 +0200 Subject: [PATCH 2/3] Address review feedback for knapsack_with_count --- knapsack/knapsack.py | 12 ++++++++---- knapsack/tests/test_knapsack.py | 4 +++- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/knapsack/knapsack.py b/knapsack/knapsack.py index dfcfd5dd53cb..944c78e13605 100644 --- a/knapsack/knapsack.py +++ b/knapsack/knapsack.py @@ -37,7 +37,7 @@ def knapsack( got the weight of 10*5 which is the limit of the capacity. """ - @lru_cache + @lru_cache(maxsize=None) def knapsack_recur(capacity: int, counter: int) -> int: # Base Case if counter == 0 or capacity == 0: @@ -70,10 +70,14 @@ def knapsack_with_count( allow_repetition: bool = False, ) -> tuple[int, int]: """ - Return both the maximum knapsack value and the number of optimal subsets. + Return both the maximum knapsack value and number of optimal selections. - The return value is ``(max_value, number_of_optimal_subsets)``. + The return value is ``(max_value, number_of_optimal_selections)``. If multiple choices produce the same maximum value, their counts are added. + Distinct selections are order-insensitive: + - with ``allow_repetition=False`` these are distinct subsets by item index; + - with ``allow_repetition=True`` these are distinct multisets by item index + multiplicity. >>> cap = 50 >>> val = [60, 100, 120] @@ -89,7 +93,7 @@ def knapsack_with_count( (2, 2) """ - @lru_cache + @lru_cache(maxsize=None) def knapsack_recur(remaining_capacity: int, item_count: int) -> tuple[int, int]: # Base Case: one empty subset yields value 0. if item_count == 0 or remaining_capacity == 0: diff --git a/knapsack/tests/test_knapsack.py b/knapsack/tests/test_knapsack.py index 3c894a8e6fc1..5f7ba231b59f 100644 --- a/knapsack/tests/test_knapsack.py +++ b/knapsack/tests/test_knapsack.py @@ -60,7 +60,7 @@ def test_knapsack_repetition(self): def test_knapsack_with_count(self): """ - test for maximum value and number of optimal subsets + test for maximum value and number of optimal selections """ cap = 50 val = [60, 100, 120] @@ -68,6 +68,8 @@ def test_knapsack_with_count(self): c = len(val) assert k.knapsack_with_count(cap, w, val, c) == (220, 1) assert k.knapsack_with_count(cap, w, val, c, True) == (300, 1) + assert k.knapsack_with_count(0, w, val, c) == (0, 1) + assert k.knapsack_with_count(50, w, val, 0) == (0, 1) def test_knapsack_with_count_ties(self): """ From b5c47ee6240c51eb4a885399b751fcdbd380f933 Mon Sep 17 00:00:00 2001 From: nickzerjeski Date: Sat, 11 Apr 2026 16:56:13 +0200 Subject: [PATCH 3/3] Use functools.cache for knapsack memoized helpers --- knapsack/knapsack.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/knapsack/knapsack.py b/knapsack/knapsack.py index 944c78e13605..fb89c1a4f194 100644 --- a/knapsack/knapsack.py +++ b/knapsack/knapsack.py @@ -4,7 +4,7 @@ from __future__ import annotations -from functools import lru_cache +from functools import cache def knapsack( @@ -37,7 +37,7 @@ def knapsack( got the weight of 10*5 which is the limit of the capacity. """ - @lru_cache(maxsize=None) + @cache def knapsack_recur(capacity: int, counter: int) -> int: # Base Case if counter == 0 or capacity == 0: @@ -93,7 +93,7 @@ def knapsack_with_count( (2, 2) """ - @lru_cache(maxsize=None) + @cache def knapsack_recur(remaining_capacity: int, item_count: int) -> tuple[int, int]: # Base Case: one empty subset yields value 0. if item_count == 0 or remaining_capacity == 0: