Ch-4 Knapsack

Reading Time: 5 minutes
 

Classical Greedy Problem

Continuing with our Greedy Strategy we examine a classical problem of Knapsack.

The problem is relatively simple,

Given N items with different weights, find the maximum weight we can fit into a Knapsack of capacity C.

 

Naive Solution

First let’s think of a very simple solution.

  • We start with one item and if we can fit that item in our Knapsack we keep it.
  • We examine the remaining items one by one and keep the item if we can place it in our Knapsack.
  • Once this process is over, we repeat the above process but starting with an item we’ve not already started with in the past.

The Naive solution is bound to give us the correct solution since we examine each possible way in which we can keep items in the knapsack and then we can figure out the maximum value of Knapsack by finding the largest one.

It turns out though this is really really slow. The following code snippet shows how it can be done in Go. 

The code uses a recursive strategy to find out the value of a knapsack and then compares it to the current maximum knapsack value.

var callStack int
var DoPrint string

//This is for printing the call stack in a manner
//that it's readable and mapped to each call of the
//recursive function.
func init() {
        callStack = 0
}

func printSpaces() {
        if DoPrint == "" {
                return
        }
        for i := 0; i < callStack; i++ {
                fmt.Printf("-")
        }
}

//We need a way for us to bind value and weight so we can
//manipulate them together instead of working in indices always.
//This is ofcourse an internal structure
type item struct {
        weight int
        value  int
        seen   bool
}

func knapsackNaiveDriver(capacity int, items []item) int {
        maxValue := 0
        if capacity <= 0 {
                return 0
        }
        callStack++
        printSpaces()
        if DoPrint != "" {
                fmt.Printf(">Calculating maximum for size %d\n", capacity)
        }

        for i := 0; i < len(items); i++ {
                //Don't look at the item already seen so far.
                if items[i].seen {
                        continue
                }
                if items[i].weight <= capacity {
                        items[i].seen = true
                        printSpaces()
                        if DoPrint != "" {
                                fmt.Printf(">Selected %d\n", items[i].value)
                        }
                        callStack++
                        lesserBag := knapsackNaiveDriver(capacity-items[i].weight, items)
                        if lesserBag+items[i].value > maxValue {
                                maxValue = lesserBag + items[i].value
                                printSpaces()
                                if DoPrint != "" {
                                        fmt.Printf("Picking item %d with value %d, currentMax = %d\n",
                                                i, items[i].value, maxValue)
                                }
                        }
                        items[i].seen = false
                        callStack--
                }
        }
        callStack--
        return maxValue

}

func KnapsackNaive(capacity int, weights, values []int) int {
        var items []item
        for i := 0; i < len(weights); i++ {
                it := item{weight: weights[i], value: values[i], seen: false}
                items = append(items, it)
        }
        return knapsackNaiveDriver(capacity, items)
}
 

Correcting the Slowness

The slowness of the algorithm above arises from the fact that we are recomputing some of the values a lot of times which is what making it slower. Let’s take an example

 

Item 1Item 2Item 3Item 4Item 5

If we start with Item 1, then for the remaining Knapsack capacity we can choose from the rest of the items. Considering there are N such items we can say that for every Knapsack capacity remaining we’ll either be able to choose an item or not. Thus,

MAP[SIZE] = MAP[SELECTED ITEMS]

That is, for a particular knapsack size we’ll have a map of values where an item may or may not be present.

If we consider items to be bits of a number then we can describe selected items using a number.

Though this will make things a quite a bit faster but we would require auxiliary storage, in addition the number of items might be large and we might’ve to use a String representation of selected items since it may not fit into a number, for example say N=1000 items

 

Employing a Greedy Strategy

Since we want to maximize the Knapsack value a simple change to our algorithm can do the trick. Why not pick up the item with largest value we can fit in knapsack everytime?

Well let’s try it out, assume that we’ve the following items 

ItemItem 1style="color:white;background-color:red;">Item 2style="color:white;background-color:green;">Item 3
Value978
Weight534

If the Knapsack Capacity is 7 what will be the largest Knapsack value?

If we try with our current greedy algorithm that gives us 9 (Item 1) however we  can see that it’s actually 15 (Item 2 + Item 3). So we need a little tweak in our greedy algorithm.

Most of the time there are going to be more than 1 constraint to apply to our greedy algorithm. We just need to find out a mathematical model to fit those constraints into. In the case of Knapsack we want to pick the item which has lowest weight and highest value. Thus if we pick items based on the maximum Value / Weight we’ll have our greedy algorithm correct.

In the above table, the Value / Weight is in the following order

  • Item 2 ( 7 / 3)
  • Item 3 (8 / 4)
  • Item 1 (9 / 5)

So our greedy algorithm works out as follows

  • Find the item which has maximum Value / Weight
  • If we can keep this item in Knapsack, i.e. Weight <= current capacity then keep it
  • Repeat this process  until we’ve looked over all items or we don’t have any space left in Knapsack.

Unfortunately while this works for fractional Knapsack, i.e. where we can even take piece of an item not whole. This doesn’t work when we want to pick up the whole item. The simple reason being the Lemma isn’t satisfied that we’ll always pickup the item with the largest value / weight value since it’s possible we’re not able to pick up the item wholly.

Faster Algorithm when choosing item Wholly

For every item there are exactly two possibilities

  • We pick that item and it increases the value of knapsack.
  • We don’t pick that item and check remaining items.

In pseudo code we can do something like

  • Check if we can pick the item
  • If we can then calculate the new value of knapsack as this item + largest value of remaining knapsack capacity.
  • If we can’t pick up this item then for this capacity maximum value will be that of the remaining elements with same capacity we started with when we checked this item.
  • Check the bigger of two and set that as maximum for this capacity.

The KnapsackNaive solution is slow because it re-calculates a lot of subproblems. We can speed this up by saving these calculations since for a given capacity and a set of items this value can’t change

NOTE: We need to do this for every capacity we can encounter. The following code snippet is in go.

func knapsackFastDriver(capacity int, items []item) int {
        maxValue := 0
        callDepth += 1
        defer decreaseAndSetCallDepth()

        if len(items) == 0 || capacity == 0 {
                hits += 1
                return maxValue
        }
        if knapsackFastMap == nil {
                knapsackFastMap = make(map[int]map[int]int)
        }

        if _, ok := knapsackFastMap[capacity]; ok {
                if _, ok = knapsackFastMap[capacity][len(items)]; ok {
                        hits += 1
                        return knapsackFastMap[capacity][len(items)]
                }
        }
        miss += 1

        //If we're able to pick this item in knapsack.
        max1 := 0
        if items[0].weight <= capacity {
                lesserKnapsackValue := knapsackFastDriver(capacity-items[0].weight, items[1:])
                max1 = items[0].value + lesserKnapsackValue
        }
        //If we're not able to pick this item in knapsack
        max2 := knapsackFastDriver(capacity, items[1:])

        //Choose the bigger of when we pick and when we don't
        if max1 > max2 {
                maxValue = max1
        } else {
                maxValue = max2
        }
        if _, ok := knapsackFastMap[capacity]; !ok {
                knapsackFastMap[capacity] = make(map[int]int)
        }
        knapsackFastMap[capacity][len(items)] = maxValue
        return maxValue
}
 

You can follow along with the complete code at

GO-SORT GITHUB Repository

Note: The Naive solution is very slow and benchmarking it for anything beyond 50 items may not work.

Leave a Reply