Algorithm – Knapsack

This is one of the optimization algorithm where we try lot of options and optimize (maximize or minimize based on the requirement) the output. For this algorithm, we will be given list of items, their size/weight and their values. We have a knapsack (bag pack) with a limited size/weight and we need to find the best choice of items to fit those in the knapsack so that we can maximize the value.

One of the examples where we can use this algorithm is while preparing for travelling (back packing for trekking or etc). We wish we can take everything but we will have limited backpack and need to choose the best items. One other classic example is a thief trying to rob a house or a shop. He can carry limited items and he needs to take the best choice so that he can maximize the value. (If he is a programmer, I am sure he will use this algorithm ;))

Here we will discuss only the classic flavor of knapsack problem, which is called 0-1 knapsack. It means, we can either take the full item or not. We can’t take the part of the item available.

Problem

Let us formalize the problem statement here. The input contains three parameters, price[], weights[] and  sackCapacity.

  • price[] – this array contains the prices/values of items
  • weights[] – this array holds the weights/size of the items
  • sackCapacity – this is the knapsack (backpack) capacity

So price[i] contains the price of ith item and weights[i] is the weight.

Solution

This knapsack problem can be solved with different approaches, but here we will explore the approach by an algorithm called Dynamic Programming (DP).

DP is all about breaking the original problem into sub problems, solving them and remember/store the solutions and then building the solution for original problem. In this case, we will find the best suitable or choice of items for weights from 1 to sackCapacity-1 and then we can easily build the solution for sackCapacity.

There are two different DP approaches,

  • Top down DP (Recursive) – here we start calculating solution for original problem using solutions of sub problems and where the solutions for sub problems are achieved by performing recursive calls.
  • Bottom up approach (Iterative) – here we find the solutions to smaller problems and then iteratively build the solution for original problem. For example, to calculate the Nth Fibonacci number, we start from finding 0, 1, 2 … N-1 Fibonacci numbers and then build Nth Fibonacci number. There are no recursive calls in this.

Even most of the modern computers perform poorly if Top down approach is used as it uses stack memory for recursive calls so here we will discuss the bottom up approach. As most of the DP approaches are better explained using examples rather than the algorithm itself, we will also follow the same approach to understand it better.

In simple words, we check each item and see whether we are getting the better value if we include this item in the knapsack, if yes, then take that item. If not, then skip this item.

Example

Total items – 6
Prices – {6, 4, 5, 3, 9, 7}
Weights – {4, 2, 3, 1, 6, 4}
Knapsack capacity – 10

So here all of our sub problems are – knapsack capacity with 0, 1 2, 3, 4, 5, 6, 7, 8, 9 and original problem is for knapsack capacity with 10.

As per the DP, we need to find the solutions for these sub problems and store them in a table. For this, we can take a double dimension array  – int[nItems +1][sackCapacity+1] (let us call it dpTable) where nItems is the number of items and sackCapacity is the knapsack capacity. In this, dpTable[i][w] – represents the optimal solution for i items for sack capacity weight w.

Then we need to initialize the array with 0 for first row and column as optimal solutions for 0 items with any sack capacity is zero and also for any number of items with 0 sack capacity. After the initialization, the dpTable looks like below.

Initial Knapsack DP table

Initial Knapsack DP table

Then, we perform the following steps iteratively for sack capacity from 1 to 10 (sackCapacity)

  • Check the weight of the each item, if it can’t fit (weights[i] > w) into our sack, then we move to the next weight
  • If we can fit it in (weights[i] <= w), then we calculate the optimal price by the following steps.
    • Find the price if we include this item. We just wanted to know what the price gain is if we include this in the sack. So price = price of this item + price of (sackCapacity of this sub problem – weight of this item)
    • Find the price if we don’t include this item – it will be optimal price of i-1 items.
    • Then take the best option from above – the greater price.

So the final dpTable will look like below for the above example.

Final DP Table for Knapsack

Final DP Table for Knapsack

Algorithm in Java

Following method shows the implementation of how we perform the above mentioned steps to pick the optimal items.

  /**
   * price[] - value - $$$ Gain weights[] - weights sackCapacity - the maximum
   * weight knapsack can carry.
   */
  private static boolean[][] getItemsToPick(int[] price, int[] weights, int sackCapacity) {
    int nItems = price.length;
    //dp[i][w] - the maximum value of sub problem with i items and with w sack capacity.
    //no need to initialize with zeros as in java, the defalt values are 0 for int premitive type.
    int[][] dpTable = new int[nItems + 1][sackCapacity + 1];
    boolean[][] keep = new boolean[nItems][sackCapacity + 1];

    //iterate through all of the items
    for (int i = 1; i <= nItems; i++) {
      //calculate sub problem for all weights
      for (int w = 1; w <= sackCapacity; w++) {
        if (weights[i - 1] > w) {
          // we cannt take this weight as it exceeds sub problem with weight w and i items
          dpTable[i][w] = dpTable[i - 1][w];
        } else {
          //Price if we include item i
          int pYes = price[i - 1] + dpTable[i - 1][w - weights[i - 1]];
          //Price if we include item i
          int pNo = dpTable[i - 1][w];
          if (pYes > pNo) {
            //this item MAY go into sack
            keep[i - 1][w] = true;
            dpTable[i][w] = pYes;
          } else {
            dpTable[i][w] = pNo;
          }
        }
      }
    }
    //printing dpTable
    System.out.println(Arrays.deepToString(dpTable));
    return keep;
  }

To print what items we picked, we need to keep track these choices in another boolean array. For this, we have used boolean[][] keep. Following java code backtracks the items we picked up and prints the details.

  public static void printSelectedItems(boolean[][] keep, int sackCapacity, int[] price, int[] weights) {
    //printing what items we picked
    System.out.println("Selected items:");
    int K = sackCapacity;
    int n = price.length;
    int wsel = 0;
    int vsel = 0;
    for (int i = n - 1; i >= 0; i--) { // need to go in the reverse order
      if (keep[i][K] == true) {
        System.out.println(i + "\tv=" + price[i] + "\tw=" + weights[i]);
        wsel += weights[i];
        vsel += price[i];
        K = K - weights[i];
      }
    }
    System.out.println("The overall value of selected items is " + vsel + " and weight " + wsel);
    System.out.println("Maximum weight was " + sackCapacity);
  }

Time Complexity

  • The time complexity is O(NW). Here N – is the number of items and W is the weight of the knapsack. As it is depends on number of items and also on weights, we call this pseudo polynomial. It is linear in terms of number of items but remember, W – the sack capacity can be much much bigger than N. so this is called pseudo-polynomial.
  • Space, we used O(N2) to store the DP table and same memory to keep track of what items we picked up. Actually we don’t need to have this extra keep table to backtrack items we picked up.

Following is the full java implementation of the above approach.

package com.sada.algorithms.dp;

import java.util.Arrays;

/**
 * Knapsack algorithm
 *
 * @author Sada Kurapati <sadakurapati@gmail.com>
 */
public class Knapsack {

  public static void main(String[] args) {
    int[] price = {6, 4, 5, 3, 9, 7};
    int[] weights = {4, 2, 3, 1, 6, 4};

    int sackCapacity = 10;
    boolean[][] keep = getItemsToPick(price, weights, sackCapacity);
    printSelectedItems(keep, sackCapacity, price, weights);
  }

  /**
   * price[] - value - $$$ Gain weights[] - weights sackCapacity - the maximum
   * weight knapsack can carry.
   */
  private static boolean[][] getItemsToPick(int[] price, int[] weights, int sackCapacity) {
    int nItems = price.length;
    //dp[i][w] - the maximum value of sub problem with i items and with w sack capacity.
    //no need to initialize with zeros as in java, the defalt values are 0 for int premitive type.
    int[][] dpTable = new int[nItems + 1][sackCapacity + 1];
    boolean[][] keep = new boolean[nItems][sackCapacity + 1];

    //iterate through all of the items
    for (int i = 1; i <= nItems; i++) {
      //calculate sub problem for all weights
      for (int w = 1; w <= sackCapacity; w++) {
        if (weights[i - 1] > w) {
          // we cannt take this weight as it exceeds sub problem with weight w and i items
          dpTable[i][w] = dpTable[i - 1][w];
        } else {
          //Price if we include item i
          int pYes = price[i - 1] + dpTable[i - 1][w - weights[i - 1]];
          //Price if we include item i
          int pNo = dpTable[i - 1][w];
          if (pYes > pNo) {
            //this item MAY go into sack
            keep[i - 1][w] = true;
            dpTable[i][w] = pYes;
          } else {
            dpTable[i][w] = pNo;
          }
        }
      }
    }
    //printing dpTable
    System.out.println(Arrays.deepToString(dpTable));
    return keep;
  }

  public static void printSelectedItems(boolean[][] keep, int sackCapacity, int[] price, int[] weights) {
    //printing what items we picked
    System.out.println("Selected items:");
    int K = sackCapacity;
    int n = price.length;
    int wsel = 0;
    int vsel = 0;
    for (int i = n - 1; i >= 0; i--) { // need to go in the reverse order
      if (keep[i][K] == true) {
        System.out.println(i + "\tv=" + price[i] + "\tw=" + weights[i]);
        wsel += weights[i];
        vsel += price[i];
        K = K - weights[i];
      }
    }
    System.out.println("The overall value of selected items is " + vsel + " and weight " + wsel);
    System.out.println("Maximum weight was " + sackCapacity);
  }
}
Image

Algorithm – Tower of Hanoi Game with K Towers

The standard Tower of Hanoi game is with 3 towers and we need to move all discs from one tower to another. This post provides a flavor of same puzzle but with more than 3 towers and given an initial configuration, we need to find the minimum moves required to go to a target configuration.

Problem

We will be given the discs with radius 1 to N, towers (K), an initial configuration and target configuration. We need to calculate the minimum number of moves we need to make to get to the target configuration starting from initial configuration.

Few rules:

  • N discs – there are N discs each with radius of 1 to N
  • K towers
  • Disc with radius R can be added to the tower if and only if it is empty or the top disc’s radius is < R
  • 1<= N<=8 and 3<= K<=5 – these constraints help us in building a more practical algorithm
  • Read the input from STDIN – System.in and print the output to STDOUT – System.out

Input format

  • First line consists of two integers N and K with space delimited
  • 2nd line consists of initial configuration. It is provided as space delimited integers where index is the disc radius and the number at that index is the tower it contains
  • 3rd line consists of target or desired configuration. It is provided in the same initial configuration.

Sample Input

6 4
4 2 4 3 1 1
1 1 1 1 1 1
Here,

  • N=6 (discs) and K=4 (towers)
  • Initial configuration is “4 2 4 3 1 1”
    • Tower 1 have 6 and 5 discs
    • Tower 2 have disc 2
    • Tower 3 have disc 4
    • tower 4 have 3 and 1 discs
  • Target configuration is “1 1 1 1 1 1”
    •  all discs from 6 to 1 are on tower 1

Following diagram depicts the source and target Tower of Hanoi configurations. Note that the discs are not according to the radius, but they are color coded to represent appropriate radius for simplicity.

tower_of_hanoi

Output format

  • 1st line should be the minimum number of moves required to get to target configuration from initial configuration
  • From 2nd line, print from and to tower for a given move. “4 3”  – means a disc is moved from 4th tower to 3rd tower

Sample output

Following is the output for above given sample input.
5
3 1
4 3
4 1
2 1
3 1
Here,

  • 5 – minimum number of move to get to target configuration
  • Then each line consists of from and to towers for each of those 5 moves. For example, “3 1” represents a disc is moved from tower 3 to 1

Solution

There are different ways to solve this problem. Here we will discuss a simple and object oriented approach. We can model this solution as follows.

  • This can be solved using a Breadth-First-Search (BFS). Why BFS? It is the best way to travel a level by level and find the shortest path from a given node to target node
  • Consider the initial configuration as the starting node
  • Then get all of its neighboring nodes – all possible legal configuration we can get from current configuration and repeat this step until we reach target configuration or the graph is exhausts
  • Towers can be easily represent as a Stack
  • Node contains K towers – it represents a configuration

In Java

Following is the full java implementation of the above approach. Please leave your feedback in comments section.

package com.sada.puzzles;

import com.sada.puzzles.TOH.Node.Move;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Scanner;
import java.util.Set;
import java.util.Stack;
import java.util.TreeMap;

/**
 *
 * @author Sada Kurapati <sadakurapati@gmail.com>
 */
public class TOH{

  public static void main(String args[]) {
    Scanner sc = new Scanner(System.in);
    int n = sc.nextInt();
    int k = sc.nextInt();
    //towers initial config
    Node source = readPegsConfiguration(k, n, sc);
    //read target configuration
    Node target = readPegsConfiguration(k, n, sc);
    //To keep track what config we visited and avoid cycles
    Set visited = new HashSet();
    try {
      minMovesToTarget(source, target, visited);
    } catch (Exception ex) {
      System.out.println("Exception = " + ex);
    }
  }

  private static void minMovesToTarget(Node source, Node target, Set visited) throws CloneNotSupportedException {
    //Perform BFS
    //add source node to Queue
    Queue q = new LinkedList();
    q.add(source);
    Node current = source;
    while (!q.isEmpty()) {
      current = q.poll();
      if (current.equals(target)) { //found the target configuration
        break;
      }
      List neighbors = current.neighbors();
      if (neighbors.size() > 0) {
        for (Node n : neighbors) {
          if (!visited.contains(n)) {//if not visited, put it in queue
            q.offer(n);
            visited.add(n);
          }
        }
      }
    }
    //Printing path and moves if target config found
    if (current.equals(target)) {
      printOutput(current);
    }
  }

  private static Node readPegsConfiguration(int k, int n, Scanner sc) {
    Stack[] initialState = new Stack[k];
    for (int i = 0; i < k; i++) {
      initialState[i] = new Stack();
    }
    //reading and reversing the line as we need to put the elements in decresing size
    //disc is key and tower is value.
    TreeMap<Integer, Integer> map = new TreeMap<Integer, Integer>(Collections.reverseOrder());
    for (int i = 0; i < n; i++) {
      map.put(i, sc.nextInt());
    }
    //prepare towers
    for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
      initialState[entry.getValue() - 1].push(entry.getKey());
    }
    return new Node(initialState);
  }

  static void printOutput(Node target) {
    Stack stack = new Stack<>(); //using stack as we need to print the trail from Source - target config
    while (target.parent != null) {
      stack.add(target.move);
      target = target.parent;
    }
    System.out.println(stack.size());
    while (!stack.isEmpty()) {
      System.out.println(stack.pop());
    }
  }

  static class Node implements Cloneable {
    //towers
    Stack[] state = null;
    Node parent = null;  //for backtracking trail
    Move move = null; // The move we made to go to next neighbor config

    public Node(Stack[] st) {
      state = st;
    }

    @Override
    protected Node clone() throws CloneNotSupportedException {
      Stack[] cloneStacks = new Stack[state.length];
      for (int i = 0; i < state.length; i++) {
        cloneStacks[i] = (Stack) state[i].clone();
      }
      Node clone = new Node(cloneStacks);
      return clone;
    }

    //returns the neghboring configurations.
    //What all configurations we can get based on current config.
    public List neighbors() throws CloneNotSupportedException {
      List neighbors = new ArrayList<>();
      int k = state.length;
      for (int i = 0; i < k; i++) {
        for (int j = 0; j < k; j++) {
          if (i != j && !state[i].isEmpty()) {
            //Need to clone to avoid change the parent node.
            //Hint - in java, objects are not mutable and they are references
            Node child = this.clone();
            //make a move
            if (canWeMove(child.state[i], child.state[j])) {
              child.state[j].push(child.state[i].pop());
              //this is required to backtrack the trail once we find the target config
              child.parent = this;
              //the move we made to get to this neighbor
              child.move = new Move(i, j);
              neighbors.add(child);
            }
          }
        }
      }
      return neighbors;
    }

    public boolean canWeMove(Stack fromTower, Stack toTower) {
      boolean answer = false;
      if (toTower.isEmpty()) {// if destination tower is empty, then we can move any disc
        return true;
      }
      int toDisc = toTower.peek();
      int fromDisc = fromTower.peek();
      if (fromDisc < toDisc) { //we can only place small disc on top
        answer = true;
      }
      return answer;
    }

    @Override
    public int hashCode() {
      int hash = 7;
      return hash;
    }

    @Override
    public boolean equals(Object obj) {
      if (obj == null) {
        return false;
      }
      if (getClass() != obj.getClass()) {
        return false;
      }
      final Node other = (Node) obj;
      if (!Arrays.deepEquals(this.state, other.state)) {
        return false;
      }
      return true;
    }

    class Move {
      int towerFrom, towerTo;
      public Move(int f, int t) {
        towerFrom = f + 1;
        towerTo = t + 1;
      }
     @Override
      public String toString() {
        return towerFrom + " " + towerTo;
      }
    }
  }
}