Given n items with their respective weights (w1, w2, ..., wn) and values (v1, v2, ..., vn), along with a total capacity weightW of the knapsack, the goal is to maximise the total value of items placed in the knapsack. Each item can be used at most once, and the total weight of selected items must not exceed the knapsack’s capacity.
Input:
Weights =[2,3,4], Values =[4,5,6], Capacity =5Output:
Maximum value =9Explanation:
- We include item1(weight=2, value=4) and item2(weight=3, value=5),achieving a total weight=5 and total value=9.
Input:
Weights =[1,2,3], Values =[6,10,12], Capacity =5Output:
Maximum value =22Explanation:
- We include item2(weight=2, value=10) and item3(weight=3, value=12),achieving a total weight=5 and total value=22.
Without repetitions: Each item can only be selected once, which is the current problem: 0-1 Knapsack Problem
Additionally, there are special cases of the knapsack problem:
All items have equal weight: In this scenario, the goal is to maximise profit as all items weigh the same. Sorting items by value and then picking the highest-value items will solve this variant using a greedy approach.
All items have equal value: If all items have the same value, selecting as many items as possible within the weight limit becomes the target. Sorting items by size and selecting the smallest ones until the knapsack is full provides an efficient solution.
Another prominent instance of the knapsack problem is the Subset Sum Problem, where each item’s value is proportional to its size. For example, consider a collection of gold weights: heavier weights hold more value, and the goal is to fill the knapsack while minimising leftover space. Additionally, this problem appears in cases where you aim to achieve a specific total (C) as closely as possible.
However, this subset sum problem cannot be solved using a greedy algorithm because it is NP-complete. This means it belongs to a class of problems for which no known polynomial-time solutions exist unless P = NP is proven. It is often linked to the Subset Sum Problem and related algorithms like the Integer Partition Algorithm.
Whenever an algorithmic problem like this arises, identifying patterns in the statement can help connect it to known approaches:
Choices: For each item, there are two possibilities:
Include the item (if the weight allows).
Exclude the item.
Optimisation Problem: The goal is to maximise the total value within given constraints.
The problem lends itself naturally to a recursive solution, where we break it into smaller subproblems (optimising value for smaller capacities and fewer items). Dynamic programming is an optimised recursion, storing results of computed subproblems to avoid redundant calculations.
Let Knapsack(W, n) represent the maximum value obtainable for a knapsack of capacity W using the first n items. For every item, we follow these steps:
Weight exceeds capacity: If the item’s weight is greater than the knapsack capacity (wt[n] > W), the item cannot be included. We move to the next item:
1
Knapsack(W, n) = Knapsack(W, n-1)
Weight fits in the knapsack: If the item’s weight allows it to fit (wt[n] <= W), we have two options:
Include the item: Add its value (val[n]) and solve the reduced problem for the remaining capacity (W - wt[n]).
Exclude the item: Solve the problem without it.
The result is the maximum of these two choices:
#include<iostream>#include<algorithm>usingnamespace std;
classSolution {
public:int knapsack(int capacity, int weights[], int values[], int n) {
// Base Case: No items left or capacity exhausted
if (n ==0|| capacity ==0)
return0;
// If the current item exceeds the capacity, exclude it
if (weights[n -1] > capacity)
return knapsack(capacity, weights, values, n -1);
// Otherwise, choose the maximum of including or excluding the current item
else {
int includeItem = values[n -1] + knapsack(capacity - weights[n -1], weights, values, n -1);
int excludeItem = knapsack(capacity, weights, values, n -1);
returnmax(includeItem, excludeItem);
}
}
};
// Example usage
intmain() {
Solution solution;
int weights[] = {2, 3, 4};
int values[] = {4, 5, 6};
int capacity =5;
int n =sizeof(weights) /sizeof(weights[0]);
cout <<"Maximum value: "<< solution.knapsack(capacity, weights, values, n) << endl;
return0;
}
classSolution:
defknapsack(self, W, weights, values, n):
# Base Case: No items left or capacity exhaustedif n ==0or W ==0:
return0# If the current item exceeds the capacity, exclude itif weights[n -1] > W:
return self.knapsack(W, weights, values, n -1)
# Otherwise, choose the maximum of including or excluding the current itemelse:
include_item = values[n -1] + self.knapsack(W - weights[n -1], weights, values, n -1)
exclude_item = self.knapsack(W, weights, values, n -1)
return max(include_item, exclude_item)
# Example usage:solution = Solution()
weights = [2, 3, 4]
values = [4, 5, 6]
capacity =5print("Maximum value:", solution.knapsack(capacity, weights, values, len(weights)))
⏰ Time complexity: O(2^n) where n is number of items. In the worst case, the recursion explores all subproblems, where each can be included or excluded, leading to O(2^n)
🧺 Space complexity: O(n) as recursion depth is proportional to number of items.
Memoization improves the recursive approach by storing the results of previously solved subproblems in a table. This avoids redundant computations and transforms the exponential complexity of recursion into a more efficient polynomial complexity.
In the recursive solution, we recompute results for the same inputs multiple times due to overlapping subproblems. The memoized solution addresses this issue by storing computed values in a 2D table, memo[n][W], where:
n represents the number of items being considered.
W represents the current capacity of the knapsack.
If a value for memo[n][W] is computed, it is reused instead of recalculating the result.
#include<iostream>#include<vector>usingnamespace std;
classSolution {
public:int knapsackMemo(int capacity, int weights[], int values[], int n, vector<vector<int>>& memo) {
// Base Case: No items left or capacity exhausted
if (n ==0|| capacity ==0)
return0;
// If the result for this subproblem is already computed
if (memo[n][capacity] !=-1)
return memo[n][capacity];
// If the current item's weight exceeds the remaining capacity
if (weights[n -1] > capacity) {
memo[n][capacity] = knapsackMemo(capacity, weights, values, n -1, memo);
} else {
int includeItem = values[n -1] + knapsackMemo(capacity - weights[n -1], weights, values, n -1, memo);
int excludeItem = knapsackMemo(capacity, weights, values, n -1, memo);
memo[n][capacity] = max(includeItem, excludeItem);
}
return memo[n][capacity];
}
intknapsack(int capacity, int weights[], int values[], int n) {
// Initialize memo table with -1
vector<vector<int>> memo(n +1, vector<int>(capacity +1, -1));
// Call memoized helper
return knapsackMemo(capacity, weights, values, n, memo);
}
};
// Example usage
intmain() {
Solution solution;
int weights[] = {2, 3, 4};
int values[] = {4, 5, 6};
int capacity =5;
int n =sizeof(weights) /sizeof(weights[0]);
cout <<"Maximum value: "<< solution.knapsack(capacity, weights, values, n) << endl;
return0;
}
In the bottom-up DP approach, the goal is to iteratively calculate the maximum value obtainable for each weight from 1 to W and for each item from 1 to n. This approach eliminates recursion and uses a 2-dimensional table to store intermediate results.
To find the recurrence relation, consider this: for each item i and capacity W:
Each item is either selected or not. The maximum value for a given weight W can be obtained by:
Including the item: If its weight wi allows room in the knapsack (W - wi), add its value vi to the value from the remaining weight (W - wi, considering up to item i-1).
Excluding the item: Retain the value obtained for weight W considering up to item i-1.
Knapsack(W, weights, values, n):
Initializevalue[i][j] = 0foralliandj// Base conditionforifrom1ton:
forwfrom1toW:
value[w][i] = value[w][i-1] // Exclude the itemifweights[i-1] <=w: // Item can fitval = value[w-weights[i-1]][i-1] +values[i-1]
value[w][i] = max(value[w][i], val) // Include the itemreturnvalue[W][n] // Result (maximum value for capacity W using n items)
If there are zero items (i = 0), the value is zero for any weight:
1
value(W, 0) = 0
If the capacity is zero (W = 0), the value is zero for any number of items:
1
value(0, i) = 0
Memoization improves the recursive approach by storing the results of previously solved subproblems in a table. This avoids redundant computations and transforms the exponential complexity of recursion into a more efficient polynomial complexity.
In the recursive solution, we recompute results for the same inputs multiple times due to overlapping subproblems. The memoized solution addresses this issue by storing computed values in a 2D table, memo[n][W], where:
n represents the number of items being considered.
W represents the current capacity of the knapsack.
If a value for memo[n][W] is computed, it is reused instead of recalculating the result.
classSolution:
defknapsack(self, W, weights, values, n):
# Initialize DP table (value[i][j]) with zeros dp = [[0for _ in range(W +1)] for _ in range(n +1)]
# Fill the DP tablefor i in range(1, n +1):
for w in range(1, W +1):
dp[i][w] = dp[i -1][w] # Exclude the itemif weights[i -1] <= w: # Include the item if it fits dp[i][w] = max(dp[i][w], dp[i -1][w - weights[i -1]] + values[i -1])
return dp[n][W] # Maximum value for capacity W with n items# Example usage:solution = Solution()
weights = [6, 3, 4, 2]
values = [30, 14, 16, 9]
capacity =10print("Maximum value:", solution.knapsack(capacity, weights, values, len(weights)))