# 0/1 Knapsack Problem

**Introduction**

The Knapsack problem is a classic optimization problem that involves selecting a subset of items to maximize their value while respecting a constraint on the total weight or size of the selected items. The problem gets its name from the analogy of packing a knapsack with items that have different values and weights, with the goal of maximizing the total value of the items while staying within the capacity of the knapsack.

There are several variations of the Knapsack problem, depending on the constraints and the properties of the items. The two most common types of the Knapsack problem are the 0/1 Knapsack problem, where each item can be selected only once, and the Fractional Knapsack problem, where each item can be selected in fractions. Other variations include the Bounded Knapsack problem, where there is a limit on the number of times each item can be selected, and the Multiple Knapsack problem, where there are multiple knapsacks with different capacities.

# 0/1 Knapsack problem

The 0/1 Knapsack problem is a variant of the Knapsack problem where each item can be either selected or not selected, meaning that items are either taken entirely or left entirely. The goal is to maximize the total value of the items in the knapsack while ensuring that the total weight or size of the selected items does not exceed a given capacity.

Formally, the problem can be defined as follows: given a set of n items, where each item i has a weight w[i] and a value v[i], and a knapsack with capacity W, find a subset S of the items such that the total weight of the items in S is at most W and the total value of the items in S is maximized.

The 0/1 Knapsack problem is known to be NP-hard, which means that it is computationally intractable to solve for large instances using exact algorithms. Therefore, researchers have developed various heuristic and approximation algorithms to solve the problem efficiently in practice.

One of the most well-known algorithms for solving the 0/1 Knapsack problem is the dynamic programming algorithm. The dynamic programming algorithm builds a table of subproblems that can be used to calculate the optimal solution for the problem by solving smaller subproblems. By recursively solving these subproblems and using the values computed in previous iterations, the algorithm determines the optimal value of the Knapsack for each combination of items and capacities. Finally, the algorithm selects the items that contributed to the optimal solution by backtracking through the table.

The 0/1 Knapsack problem has many practical applications, such as resource allocation, scheduling, and project planning. For example, it can be used to determine the optimal set of projects to fund, given a limited budget, and the expected return on investment for each project.

# How To Solve It Using Code

**1. Recursion**

To solve knapsack problem recursively, we need to explore all possible combinations of items that can be selected for the Knapsack. The function takes as input the set of items, their weights, their values, and the capacity of the Knapsack, and returns the maximum value that can be obtained by selecting items that do not exceed the capacity.

**Implementation**

`def knapsack_recursive(values, weights, capacity, n):`

# Base case: If no items are left or the capacity is 0, return 0

if n == 0 or capacity == 0:

return 0

# If the weight of the nth item is greater than the capacity of the Knapsack,

# the item cannot be included in the optimal solution. Hence, skip it.

if weights[n-1] > capacity:

return knapsack_recursive(values, weights, capacity, n-1)

# Otherwise, explore both the cases: include the nth item or exclude it

else:

included_value = values[n-1] + knapsack_recursive(values, weights, capacity - weights[n-1], n-1)

excluded_value = knapsack_recursive(values, weights, capacity, n-1)

# Return the maximum value that can be obtained by either including or excluding the nth item

return max(included_value, excluded_value)

Time Complexity: O(2^N)

## 2. Memoization

The function that uses recursion computes the same subproblems over and over again. For example, consider the following recursion tree:

` K(3,2)`

/ \

K(2,2) K(2,1)

/ \ / \

K(1,2) K(1,1)K(1,1) K(1,0)

Here K(1,1) is getting calculated twice.

Using memorization can help overcome this problem by creating a cache that can store the solution to the subproblem. Therefore, instead of computing it again, it will first check if it is already solved and use that value instead or else it would solve the subproblem.

**Implementation**

`def knapsack_memoization(values, weights, capacity, n, cache):`

# Base case: If no items are left or the capacity is 0, return 0

if n == 0 or capacity == 0:

return 0

# Check if the result for this function call has already been computed and stored in the cache

if (n, capacity) in cache:

return cache[(n, capacity)]

# If the weight of the nth item is greater than the capacity of the Knapsack,

# the item cannot be included in the optimal solution. Hence, skip it.

if weights[n-1] > capacity:

result = knapsack_memoization(values, weights, capacity, n-1, cache)

else:

# Otherwise, explore both the cases: include the nth item or exclude it

included_value = values[n-1] + knapsack_memoization(values, weights, capacity - weights[n-1], n-1, cache)

excluded_value = knapsack_memoization(values, weights, capacity, n-1, cache)

# Return the maximum value that can be obtained by either including or excluding the nth item

result = max(included_value, excluded_value)

# Store the result in the cache for future use

cache[(n, capacity)] = result

return result

Time complexity: O(N*C), where N is the number of items and C is the capacity of the knapsack.

Space complexity: O(N*C)

## 3. Dynamic Programming

The knapsack problem can be efficiently solved using dynamic programming. The dynamic programming approach involves building a table that stores the maximum value that can be obtained using a subset of the items and a subset of the capacity of the knapsack. Here’s how to solve the knapsack problem using dynamic programming.

**Implementation**

`def knapsack_dp(values, weights, capacity):`

n = len(values)

dp = [[0 for j in range(capacity+1)] for i in range(n+1)]

for i in range(1, n+1):

for j in range(1, capacity+1):

if weights[i-1] > j:

dp[i][j] = dp[i-1][j]

else:

dp[i][j] = max(dp[i-1][j], values[i-1] + dp[i-1][j-weights[i-1]])

selected_items = []

i, j = n, capacity

while i > 0 and j > 0:

if dp[i][j] != dp[i-1][j]:

selected_items.append(i-1)

j -= weights[i-1]

i -= 1

selected_items.reverse()

return dp[n][capacity], selected_items

Time complexity: O(N*C), where N is the number of items and C is the capacity of the knapsack.Space complexity: O(N*C)

Space complexity: O(N*C)

# Real World Applications of the Knapsack problem

The 0/1 Knapsack problem has many practical applications in various fields, including finance, logistics, and resource allocation. Here are a few examples of real-world problems that can be modeled as 0/1 Knapsack problems

## Backpacking

A backpacker wants to pack a set of items for a camping trip, where each item has a weight and a utility value. The backpacker has a limited carrying capacity and wants to pack the items that will maximize their overall utility. This problem can be modeled as a 0/1 Knapsack problem, where each item represents an item, the weight of the item represents the weight of the item, and the utility value of the item represents the value of the item. The dynamic programming algorithm can be used to find the optimal set of items to pack, given the carrying capacity and the utility values.

## Resource Allocation

A company has a limited budget and wants to invest in a set of projects that will maximize its overall return on investment. Each project has a cost and an expected return, and the company can only invest in a subset of the projects due to budget constraints. This problem can be modeled as a 0/1 Knapsack problem, where each project represents an item, the cost of the project represents the weight of the item, and the expected return represents the value of the item. The dynamic programming algorithm can be used to find the optimal set of projects to invest in, given the budget and the expected returns.

## Scheduling

A factory needs to schedule a set of jobs on a single machine, where each job has a processing time and a deadline. The goal is to maximize the number of jobs that are completed before their respective deadlines. This problem can be modeled as a 0/1 Knapsack problem, where each job represents an item, the processing time of the job represents the weight of the item, and the number of jobs completed before their deadline represents the value of the item. The dynamic programming algorithm can be used to find the optimal set of jobs to schedule, given the processing times and the deadlines.

# Conclusion

The Knapsack problem has applications in a variety of fields, such as finance, operations research, and computer science. For example, it can be used to optimize the allocation of resources in financial portfolios or to schedule jobs on a machine with limited capacity. The Knapsack problem is also used as a benchmark for testing algorithms, as it is known to be NP-hard, meaning that it is computationally intractable to solve for large instances using exact algorithms. Therefore, researchers have developed approximation algorithms and heuristics to solve the Knapsack problem efficiently in practice.

Apart from 0/1 knapsack, we also have other types. Some of them are as follows:

## 1. Unbounded Knapsack:

In the unbounded knapsack problem, there are no restrictions on the number of times an item can be selected. This means that for each item, there is an infinite supply available. The goal is still to maximize the total value of the items in the knapsack while staying within the weight constraint.

## 2. Fractional Knapsack

In the fractional knapsack problem, items can be divided into fractions, and a fraction of an item can be included in the knapsack. The goal is to maximize the total value of the items in the knapsack while staying within the weight constraint. The items are usually sorted in decreasing order of their value-to-weight ratio to solve this problem.

**Referenences**