diff --git a/documents/references.md b/documents/references.md index f42d45e319443838b4a3987c988b05562eaa833d..abdbef9a6ccf026494a00616a3ea6dec3acee12f 100644 --- a/documents/references.md +++ b/documents/references.md @@ -11,8 +11,14 @@ References new to this project. Used to move to the "Script's Directory", regardless of where the script was launched from in the terminal. <https://stackoverflow.com/a/3355423> +### Python's Implementation of Heaps +<https://docs.python.org/3.7/library/heapq.html> -## Old References +### Using Heapq as a Max Heap (Default Behavior is Min Heap) +<https://stackoverflow.com/a/48255910> + + +## Older Relevant References References linked in previous assignments. ### Logging Logic diff --git a/readme.md b/readme.md index 108426424d1fbef366bda9e02f3d23dfb399520d..cf601f64f21031ab7da6fe95d2146bcc084065b1 100644 --- a/readme.md +++ b/readme.md @@ -5,7 +5,12 @@ Implementation of the "Fractional Knapsack" algorithm. ### Pseudocode Modifications -None so far. +My "value array" (denoted **v** in pseudocode) is actually an array of dictionaries, where each dictionary contains all +relevant information for the given item. This allows me to store more data about the item, at a minor cost of: +* Some (negligible) extra memory space. +* An increased running time by a constant factor, to access the dict values. + +This "value array" is also treated as a heap, using Python's built in "heapq" library. ## Python Environment diff --git a/resources/knapsack.py b/resources/knapsack.py index a2329d7c46b7ecffa34536a9ab8bf17fa91b5bf9..9b06b194de3225ec73cd1722a37a8891ba966e69 100644 --- a/resources/knapsack.py +++ b/resources/knapsack.py @@ -10,7 +10,7 @@ Knapsack algorithm class. # System Imports. - +import heapq # User Class Imports. from resources import logging as init_logging @@ -27,6 +27,8 @@ class Knapsack(): def __init__(self): self._item_set_ = None self._max_weight_ = None + self._items_populated_ = False + self._weight_populated_ = False def set_item_set(self, item_set): """ @@ -50,6 +52,7 @@ class Knapsack(): raise KeyError('Key "cost" must be present in all items.') self._item_set_ = item_set + self._items_populated_ = True def set_max_weight(self, max_weight): """ @@ -65,3 +68,70 @@ class Knapsack(): raise ValueError('Arg must be greater than 0.') self._max_weight_ = max_weight + self._weight_populated_ = True + + def calculate_fractional_knapsack(self): + """ + Calculates optimal choices for fractional knapsack problem. + :return: Amount to take for each item, in list order. + """ + # First check that items and weight have been populated. + if not self._items_populated_ or not self._weight_populated_: + raise ValueError('Item Set and Max Weight must be populated to run calculations.') + + # Run algorithm. + return_array = [] + value_heap = [] + + # First, heapify all items in item set, by value. + # Use range so we can track index from initial item set. + for index in range(len(self._item_set_)): + return_array.append(0) + + # Get Item values. + item_benefit = self._item_set_[index]['benefit'] + item_cost = self._item_set_[index]['cost'] + + # Check if cost is 0. + if item_cost == 0: + item_value = item_benefit + else: + item_value = item_benefit / item_cost + + # Add item to heap. + # Note that value is the main thing to sort on, and added first. Then index is added, to act as a tie + # breaker. Finally, we have the dict to hold the item values we actually care about. item_value is negative + # to invert the "min heap" (heapq default) into a "max heap". + # (See Python Docs Heapq references for more info). + heapq.heappush(value_heap, ( + -item_value, + index, + { + 'index': index, + 'benefit': item_benefit, + 'cost': item_cost, + 'value': item_value, + },) + ) + + # Loop through all elements in heap, until we run out of elements or room. + remaining_weight = self._max_weight_ + while len(value_heap) > 0 and remaining_weight > 0: + + # Get top item from heap. + top_value = heapq.heappop(value_heap)[2] + item_cost = top_value['cost'] + + # Take item and remove cost from weight. + if remaining_weight < item_cost: + + # Not enough weight for full item. Only take equivalent to remaining weight. + return_array[top_value['index']] = remaining_weight + remaining_weight = 0 + + else: + # Can take full item. Take full amount. + return_array[top_value['index']] = item_cost + remaining_weight -= item_cost + + return return_array diff --git a/tests/resources/knapsack.py b/tests/resources/knapsack.py index 3ba9d2003cb92009b9b8fe0db09168410e7051d8..e9122752173894054ddccfe5eaf34e772d73daf7 100644 --- a/tests/resources/knapsack.py +++ b/tests/resources/knapsack.py @@ -42,6 +42,21 @@ class TestKnapsack(unittest.TestCase): } ] + self.item_set_2 = [ + { + 'benefit': 35, + 'cost': 3 + }, + { + 'benefit': 20, + 'cost': 2 + }, + { + 'benefit': 20, + 'cost': 2 + } + ] + def test_knapsack_creation(self): self.assertTrue(isinstance(self.knapsack, Knapsack)) @@ -86,4 +101,22 @@ class TestKnapsack(unittest.TestCase): with self.assertRaises(ValueError): self.knapsack.set_max_weight(-1) - + def test_calculate_fractional_knapsack_with_set_1(self): + self.knapsack.set_item_set(self.item_set_1) + self.knapsack.set_max_weight(10) + self.assertEqual( + self.knapsack.calculate_fractional_knapsack(), + [0, 1, 2, 6, 1], + ) + + def test_calculate_fractional_knapsack_with_set_2(self): + self.knapsack.set_item_set(self.item_set_2) + self.knapsack.set_max_weight(4) + self.assertEqual( + self.knapsack.calculate_fractional_knapsack(), + [3, 1, 0], + ) + + def test_calculate_fractional_knapsack_failure(self): + with self.assertRaises(ValueError): + self.knapsack.calculate_fractional_knapsack()