Knapsack Problem
The Knapsack Problem is a classic optimization problem that can be efficiently solved using dynamic programming. The goal is to choose a subset of items with maximum total value, subject to a weight constraint on the total weight of the chosen items.
Mathematical Formulation (0/1 Knapsack)
Given:
- items indexed from 1 to
- : weight of item
- : value of item
- : maximum weight capacity of the knapsack
Objective:
- Maximize
Subject to:
- for each
Here, is a binary variable indicating whether item is included in the knapsack.
Dynamic Programming Approach
Define as the maximum value that can be achieved with the first items and a knapsack capacity of .
State Transition:
- If item is not included.
- If item is included (only if ).
Initialization:
- for all (No items means no value).
Algorithm
- Initialize a table with dimensions , all set to zero.
- For each item from 1 to :
- For each capacity from 1 to :
- If , set .
- Otherwise, set .
- For each capacity from 1 to :
- The value will be the maximum value that fits into the knapsack.
Complexity
- Time Complexity: , where is the number of items and is the capacity.
- Space Complexity: , although it can be reduced to using a 1-dimensional array if only the value is required, not the items themselves.
Implementation
A typical dynamic programming table fills in values iteratively for increasing item counts and capacities, ensuring all sub-problems are solved in a bottom-up manner.
Brute Force Search
The brute force approach explores every combination using recursion, evaluating the total value of all possible sets of items.
def knapsack_dfs(wgt, val, i, c):
if i == 0 or c == 0:
return 0
if wgt[i-1] > c:
return knapsack_dfs(wgt, val, i-1, c)
no = knapsack_dfs(wgt, val, i-1, c)
yes = knapsack_dfs(wgt, val, i-1, c-wgt[i-1]) + val[i-1]
return max(no, yes)
Memoized Search
Enhances the brute force approach by storing results of sub-problems to avoid recomputation.
def knapsack_dfs_mem(wgt, val, mem, i, c):
if i == 0 or c == 0:
return 0
if mem[i][c] != -1:
return mem[i][c]
if wgt[i-1] > c:
return knapsack_dfs_mem(wgt, val, mem, i-1, c)
no = knapsack_dfs_mem(wgt, val, mem, i-1, c)
yes = knapsack_dfs_mem(wgt, val, mem, i-1, c-wgt[i-1]) + val[i-1]
mem[i][c] = max(no, yes)
return mem[i][c]
Space Optimization
The space complexity can be reduced by maintaining a single-dimensional array and updating values in reverse to prevent overwriting needed values for future states.
def knapsack_dp_comp(wgt, val, cap):
dp = [0] * (cap + 1)
for i in range(1, len(wgt) + 1):
for c in range(cap, 0, -1):
if wgt[i-1] <= c:
dp[c] = max(dp[c], dp[c-wgt[i-1]] + val[i-1])
return dp[cap]
Variants of the Knapsack Problem
- 0/1 Knapsack Problem: Each item can either be taken or left (cannot be broken into smaller pieces). You must decide whether to include each item in the knapsack.
- Fractional Knapsack Problem: Items can be divided into smaller parts. Thus, you can take fractions of items, which makes the problem easier and solvable by greedy algorithms.
- Bounded Knapsack Problem: Each item can be included multiple times, but only up to a certain limit.
- Unbounded Knapsack Problem: Each item can be taken multiple times without any limit.