From e604d4dad7ee5bcc07ed74adfd08ef081778e7a0 Mon Sep 17 00:00:00 2001
From: brodriguez8774 <brodriguez8774@gmail.com>
Date: Mon, 23 Sep 2019 00:01:13 -0400
Subject: [PATCH] Implement fractional knapsack algorithm

---
 documents/references.md     |  8 ++++-
 readme.md                   |  7 +++-
 resources/knapsack.py       | 72 ++++++++++++++++++++++++++++++++++++-
 tests/resources/knapsack.py | 35 +++++++++++++++++-
 4 files changed, 118 insertions(+), 4 deletions(-)

diff --git a/documents/references.md b/documents/references.md
index f42d45e..abdbef9 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 1084264..cf601f6 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 a2329d7..9b06b19 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 3ba9d20..e912275 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()
-- 
GitLab