Dynamic Programming
The art of strategy is knowing what to take and what to leave behind, especially when you can't carry everything.
Imagine you're a thief with a knapsack. You're in a room full of treasures, each with a specific weight and value. Your knapsack has a strict weight limit. How do you choose which items to steal to maximize your total profit? This is the classic 0/1 Knapsack Problem, a cornerstone of dynamic programming that teaches how to make optimal choices under constraints. "0/1" means that for each item, you have two choices: either take it (1) or leave it (0).
The most intuitive way to think about this is to try every possibility. For each item, we can either include it in our knapsack or not. This leads to a recursive solution.
knapsack(items, capacity):
knapsack(remaining_items, capacity).knapsack(remaining_items, capacity).value_of_current_item + knapsack(remaining_items, capacity - weight_of_current_item).
We return the max of these two choices.This approach has a time complexity of , as it explores two branches for each of the N items. This is far too slow for anything but a tiny number of items.
The brute-force approach is slow because it repeatedly solves the same subproblems. The state of any subproblem is defined by two things:
i items).w).This structure is a perfect fit for a 2D DP table.
dp, where dp[i][w] represents the maximum value we can achieve using items from the first i items, with a total weight capacity of w.i and each possible weight w from 0 to W:
i. If we don't take the i-th item, the maximum value is simply the best we could do with the previous i-1 items at the same capacity w. This value is dp[i-1][w].i. We can only do this if w >= weight[i-1]. If we take it, the value is value[i-1] plus the best we could do with the previous i-1 items given the remaining capacity: values[i-1] + dp[i-1][w - weights[i-1]].dp[i][w] is the maximum of these two choices.dp[n][W].def knapsack_01(weights: list[int], values: list[int], W: int) -> int:
n = len(weights)
dp = [[0] * (W + 1) for _ in range(n + 1)]
for i in range(1, n + 1):
for w in range(1, W + 1):
# The current item's weight and value
weight = weights[i-1]
value = values[i-1]
# If the item is too heavy, we can't include it.
if weight > w:
dp[i][w] = dp[i-1][w]
else:
# The max of either not including the item, or including it.
dp[i][w] = max(dp[i-1][w], value + dp[i-1][w - weight])
return dp[n][W]
public int knapsack01(int[] weights, int[] values, int W) {
int n = weights.length;
int[][] dp = new int[n + 1][W + 1];
for (int i = 1; i <= n; i++) {
for (int w = 1; w <= W; w++) {
int weight = weights[i-1];
int value = values[i-1];
if (weight > w) {
dp[i][w] = dp[i-1][w];
} else {
dp[i][w] = Math.max(dp[i-1][w], value + dp[i-1][w - weight]);
}
}
}
return dp[n][W];
}
int knapsack01(const std::vector<int>& weights, const std::vector<int>& values, int W) {
int n = weights.size();
std::vector<std::vector<int>> dp(n + 1, std::vector<int>(W + 1, 0));
for (int i = 1; i <= n; i++) {
for (int w = 1; w <= W; w++) {
int weight = weights[i-1];
int value = values[i-1];
if (weight > w) {
dp[i][w] = dp[i-1][w];
} else {
dp[i][w] = std::max(dp[i-1][w], value + dp[i-1][w - weight]);
}
}
}
return dp[n][W];
}
This is similar to the 0/1 Knapsack, but you can take fractions of items. The greedy approach works here:
In the above variations, you can take each item at most once. A common variant is the Unbounded Knapsack problem, where you can use each item as many times as you like (like making change with an unlimited supply of each coin type).
The change to the DP recurrence is subtle but powerful. When you decide to take an item, you can still take that same item again.
dp[i][w] = max(dp[i-1][w], ... + dp[i-1][...]) (looks at the row above for the second term).dp[i][w] = max(dp[i-1][w], ... + dp[i][...]) (looks at the current row for the second term, allowing the item to be reused).
This is the key to solving problems like "Coin Change 2."Now that you understand the core Knapsack pattern, you can see it hidden in other problems.
total_sum / 2, and the "value" of each item is the same as its "weight" (the number itself). You're trying to see if you can perfectly fill the knapsack.+ or - to numbers in an array to reach a target sum?
💡 HintThis can be transformed into a target sum problem, which is a variation of the Knapsack pattern.