Dynamic Programming Pattern: Knapsack-like
Published:
This pattern has two sub-patterns namely, 0-1 Knapsack and Unbounded Knapsack. Together, the type of problems that match these patterns are called “Knapsack-like” problems.
Pattern: 0-1 Knapsack
Overview:
The 0/1 Knapsack pattern is one of the most fundamental dynamic programming problems. In this pattern, you are given a set of items, each with a weight and a value, and a knapsack with a limited capacity. The goal is to maximize the value of the items in the knapsack without exceeding its capacity. The “0/1” refers to the fact that each item can either be included in the knapsack (1) or excluded (0).
This pattern can be extended to problems where you need to make decisions under constraints, such as selecting courses with a certain number of credit hours, choosing projects with a fixed number of resources, or dividing a set of numbers into subsets with equal sums.
Key Concepts:
- Decision Making: At each step, you must decide whether to include or exclude an item (or option) based on the problem’s constraints.
- State Representation: The state is typically represented by the remaining capacity of the knapsack and the items under consideration.
- Recursive Relation: The problem can be broken down recursively by considering two scenarios: including the current item or excluding it.
Code Example: Partition Equal Subset Sum
Let’s explore the “Partition Equal Subset Sum” problem, where we need to determine if a given set of numbers can be partitioned into two subsets with equal sums.
Problem Statement:
Given a non-empty array nums
containing only positive integers, find if the array can be partitioned into two subsets such that the sum of elements in both subsets is equal.
Code Implementation:
#include <vector>
#include <numeric>
#include <iostream>
bool canPartition(std::vector<int>& nums) {
int totalSum = std::accumulate(nums.begin(), nums.end(), 0);
// If the total sum is odd, it's impossible to partition it into two equal subsets
if (totalSum % 2 != 0) {
return false;
}
int target = totalSum / 2;
int n = nums.size();
std::vector<std::vector<bool>> dp(n + 1, std::vector<bool>(target + 1, false));
// Initialize DP table: we can always have a sum of 0 with an empty subset
for (int i = 0; i <= n; ++i) {
dp[i][0] = true;
}
// Fill the DP table
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= target; ++j) {
if (nums[i - 1] <= j) {
dp[i][j] = dp[i - 1][j] || dp[i - 1][j - nums[i - 1]];
} else {
dp[i][j] = dp[i - 1][j];
}
}
}
return dp[n][target];
}
int main() {
std::vector<int> nums = {1, 5, 11, 5};
if (canPartition(nums)) {
std::cout << "Can partition into equal subsets." << std::endl;
} else {
std::cout << "Cannot partition into equal subsets." << std::endl;
}
}
Explanation:
- State Representation:
- The DP table
dp[i][j]
represents whether it is possible to achieve a sumj
using the firsti
items.
- The DP table
- Base Case:
- We can always achieve a sum of
0
by excluding all items, hencedp[i][0] = true
for alli
.
- We can always achieve a sum of
- Recursive Relation:
- For each item
i
and target sumj
, we have two choices:- Exclude the current item: In this case,
dp[i][j] = dp[i-1][j]
. - Include the current item: Check if the remaining sum
j - nums[i-1]
can be achieved with the previous items, i.e.,dp[i][j] = dp[i-1][j-nums[i-1]]
.
- Exclude the current item: In this case,
- For each item
- Final Answer:
- The final answer is stored in
dp[n][target]
, wheren
is the number of items andtarget
is half of the total sum of the array.
- The final answer is stored in
Problems:
Pattern 2: Unbounded Knapsack
Overview:
The Unbounded Knapsack pattern is a variation of the 0/1 Knapsack problem where you are allowed to include an unlimited number of instances of each item. This pattern is applicable in scenarios where repetition is allowed, such as cutting rods into pieces, making change with coins, or maximizing profit with unlimited resources.
Key Concepts:
- Decision Making: At each step, you must decide whether to include an item in the knapsack, and unlike the 0/1 Knapsack, you can include the same item multiple times.
- State Representation: The state is represented by the current capacity of the knapsack and the items considered so far.
- Recursive Relation: The recursive relationship accounts for the possibility of reusing the same item multiple times.
Code Example: Coin Change
Let’s explore the “Coin Change” problem, where you need to determine the minimum number of coins needed to make up a given amount.
Problem Statement:
Given an array of integers coins
representing different denominations of coins and an integer amount
representing a total amount of money, return the fewest number of coins needed to make up that amount. If that amount cannot be made up by any combination of the coins, return -1
.
Code Implementation:
#include <vector>
#include <algorithm>
#include <climits>
#include <iostream>
int coinChange(std::vector<int>& coins, int amount) {
std::vector<int> dp(amount + 1, INT_MAX);
dp[0] = 0; // Base case: To make 0 amount, 0 coins are needed
for (int i = 1; i <= amount; ++i) {
for (int coin : coins) {
if (i - coin >= 0 && dp[i - coin] != INT_MAX) {
dp[i] = std::min(dp[i], dp[i - coin] + 1);
}
}
}
return dp[amount] == INT_MAX ? -1 : dp[amount];
}
int main() {
std::vector<int> coins = {1, 2, 5};
int amount = 11;
int result = coinChange(coins, amount);
if (result != -1) {
std::cout << "Minimum coins required: " << result << std::endl;
} else {
std::cout << "Amount cannot be made with given coins." << std::endl;
}
Explanation:
- State Representation:
- The DP array
dp[i]
represents the minimum number of coins needed to make the amounti
.
- The DP array
- Base Case:
- To make the amount
0
,0
coins are needed, sodp[0] = 0
.
- To make the amount
- Recursive Relation:
- For each amount
i
, we iterate over all the coins. Ifi - coin >= 0
, thendp[i]
can be updated asdp[i] = std::min(dp[i], dp[i - coin] + 1)
. This checks if the current coin can contribute to the solution and updates the minimum coins needed.
- For each amount
- Final Answer:
- The final answer is stored in
dp[amount]
. If it’s stillINT_MAX
, then the amount cannot be made with the given coins, so return-1
.
- The final answer is stored in