Pattern 3: Bounded Knapsack (Resource Allocation) Problems
The Resource Management Pattern: Make choices for items under a constraint (capacity/budget/target). State tracks items processed and resource remaining.
The Core Patternβ
Imagine you have a backpack with limited capacity and items to choose from:
- For each item, you decide: take it or leave it
- Taking it consumes resources (weight, space, budget)
- You want to optimize some value (maximize profit, minimize cost, count ways)
- You track: which items you've considered + how much resource remains
This is Bounded Knapsack DP.
Pattern Recognitionβ
Keywords: "capacity", "budget", "target sum", "select items", "weight limit", "subset", "coins"
Structure:
- Collection of items to choose from
- Limited resource (weight, capacity, sum, count)
- Each item has a cost and/or value
- Goal: optimize or count under constraint
State: dp[i][resource]
= "best answer using first i items with resource constraint"
Transition: For each item, try:
- Skip it:
dp[i-1][resource]
- Take it:
dp[i-1][resource - cost] + value
(if cost β€ resource)
Category 1: Core Knapsack Problemsβ
1. 0/1 Knapsack (Classic)β
Given n
items where each item has:
weight[i]
= weight of item ivalue[i]
= value of item i
And a knapsack with capacity W
, find the maximum value you can carry.
Constraint: Each item can be used at most once.
State Definition: dp[i][w]
= maximum value using first i
items with capacity w
Transition:
dp[i][w] = Math.max(
dp[i-1][w], // skip item i
dp[i-1][w - weight[i]] + value[i] // take item i (if fits)
);
public int knapsack(int W, int[] weights, int[] values, int n) {
int[][] dp = new int[n + 1][W + 1];
// Base case: dp[0][w] = 0 (no items = no value)
// Base case: dp[i][0] = 0 (no capacity = no value)
for (int i = 1; i <= n; i++) {
for (int w = 1; w <= W; w++) {
// Option 1: Don't take item i
dp[i][w] = dp[i-1][w];
// Option 2: Take item i (if it fits)
if (weights[i-1] <= w) {
dp[i][w] = Math.max(
dp[i][w],
dp[i-1][w - weights[i-1]] + values[i-1]
);
}
}
}
return dp[n][W];
}
This is the template for all knapsack problems:
- 2D state:
[items considered][resource remaining]
- Binary choice: skip or take
- Take only if resource constraint satisfied
Master this, and all variations become easy.
2. Partition Equal Subset Sum (LC 416)β
Given an integer array nums
, return true
if you can partition the array into two subsets such that the sum of elements in both subsets is equal.
Example: [1,5,11,5]
β true
(can partition into [1,5,5]
and [11]
)
State Definition: dp[i][sum]
= true if we can make sum
using first i
numbers
Transition:
dp[i][sum] = dp[i-1][sum] // don't use nums[i]
|| dp[i-1][sum - nums[i]] // use nums[i]
public boolean canPartition(int[] nums) {
int totalSum = 0;
for (int num : nums) totalSum += num;
// If odd sum, can't partition equally
if (totalSum % 2 != 0) return false;
int target = totalSum / 2;
int n = nums.length;
boolean[][] dp = new boolean[n + 1][target + 1];
// Base case: sum = 0 is always achievable (take nothing)
for (int i = 0; i <= n; i++) {
dp[i][0] = true;
}
for (int i = 1; i <= n; i++) {
for (int sum = 1; sum <= target; sum++) {
// Don't take nums[i-1]
dp[i][sum] = dp[i-1][sum];
// Take nums[i-1] (if it doesn't exceed sum)
if (nums[i-1] <= sum) {
dp[i][sum] = dp[i][sum] || dp[i-1][sum - nums[i-1]];
}
}
}
return dp[n][target];
}
This is 0/1 Knapsack where:
- Items = numbers in array
- Capacity = target sum (half of total)
- Goal = can we reach exactly target? (instead of max value)
Pattern Recognition: "Can we make a sum/subset?" β Think Knapsack!
3. Target Sum (LC 494)β
You are given an integer array nums
and an integer target
. You want to build an expression by adding +
or -
before each integer in nums
.
Return the number of different expressions you can build that evaluate to target
.
Example: nums = [1,1,1,1,1]
, target = 3
β 5
ways
State Definition: dp[i][sum]
= number of ways to reach sum
using first i
numbers
Transition:
dp[i][sum] = dp[i-1][sum - nums[i]] // add +nums[i]
+ dp[i-1][sum + nums[i]] // add -nums[i]
public int findTargetSumWays(int[] nums, int target) {
int sum = 0;
for (int num : nums) sum += num;
// Impossible cases
if (Math.abs(target) > sum) return 0;
if ((target + sum) % 2 != 0) return 0;
// Transform: find subset with sum = (target + sum) / 2
int subsetSum = (target + sum) / 2;
int n = nums.length;
int[][] dp = new int[n + 1][subsetSum + 1];
dp[0][0] = 1; // one way to make sum 0 with 0 items
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= subsetSum; j++) {
dp[i][j] = dp[i-1][j]; // don't use nums[i-1]
if (j >= nums[i-1]) {
dp[i][j] += dp[i-1][j - nums[i-1]]; // use nums[i-1]
}
}
}
return dp[n][subsetSum];
}
Math Trick:
- Let
P
= sum of positive numbers,N
= sum of negative numbers P - N = target
P + N = sum
(total sum)- Solving:
P = (target + sum) / 2
Problem becomes: Find subsets with sum = (target + sum) / 2
This is Partition Problem! Counting version.
4. Last Stone Weight II (LC 1049)β
State Definition: dp[i][diff]
= true if we can achieve difference diff
using first i
stones
Transition: Same as partition (minimize difference between two groups)
public int lastStoneWeightII(int[] stones) {
int sum = 0;
for (int stone : stones) sum += stone;
int target = sum / 2;
boolean[] dp = new boolean[target + 1];
dp[0] = true;
for (int stone : stones) {
for (int j = target; j >= stone; j--) {
dp[j] = dp[j] || dp[j - stone];
}
}
// Find largest achievable sum <= target
for (int j = target; j >= 0; j--) {
if (dp[j]) {
return sum - 2 * j; // difference
}
}
return 0;
}
Goal: Minimize the difference between two groups.
Same as partition, but instead of "equal", we find "closest to equal".
5. Ones and Zeroes (LC 474)β
You are given an array of binary strings strs
and two integers m
and n
.
Return the size of the largest subset of strs
such that there are at most m
0's and at most n
1's in the subset.
State Definition: dp[i][m][n]
= max size of subset using first i
strings with at most m
zeros and n
ones
Transition:
dp[i][m][n] = Math.max(
dp[i-1][m][n], // skip string i
1 + dp[i-1][m - zeros[i]][n - ones[i]] // take string i
);
public int findMaxForm(String[] strs, int m, int n) {
int len = strs.length;
int[][][] dp = new int[len + 1][m + 1][n + 1];
for (int i = 1; i <= len; i++) {
int[] count = countZeroesOnes(strs[i-1]);
int zeros = count[0];
int ones = count[1];
for (int j = 0; j <= m; j++) {
for (int k = 0; k <= n; k++) {
// Don't take string i
dp[i][j][k] = dp[i-1][j][k];
// Take string i (if enough 0s and 1s)
if (j >= zeros && k >= ones) {
dp[i][j][k] = Math.max(
dp[i][j][k],
1 + dp[i-1][j - zeros][k - ones]
);
}
}
}
}
return dp[len][m][n];
}
private int[] countZeroesOnes(String s) {
int[] count = new int[2];
for (char c : s.toCharArray()) {
count[c - '0']++;
}
return count;
}
This is 2D knapsack (two resource constraints: zeros AND ones).
Pattern Extension: Multiple resource constraints β Multiple dimensions in state.
Category 2: Coin Change Variantsβ
6. Coin Change (LC 322) - Unboundedβ
You are given an integer array coins
representing coins of different denominations and an integer amount
representing a total amount of money.
Return the fewest number of coins needed to make up that amount. Each coin can be used unlimited times.
State Definition: dp[i][amount]
= minimum coins to make amount
using first i
coin types
Transition (Unbounded):
dp[i][amount] = Math.min(
dp[i-1][amount], // don't use coin i
1 + dp[i][amount - coin[i]] // use coin i (can use again!)
);
public int coinChange(int[] coins, int amount) {
int n = coins.length;
int[][] dp = new int[n + 1][amount + 1];
// Base case: infinite coins needed for amount > 0 with 0 coin types
for (int j = 1; j <= amount; j++) {
dp[0][j] = Integer.MAX_VALUE - 1; // -1 to avoid overflow
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= amount; j++) {
// Don't use coin i
dp[i][j] = dp[i-1][j];
// Use coin i (if it fits)
if (j >= coins[i-1]) {
dp[i][j] = Math.min(
dp[i][j],
1 + dp[i][j - coins[i-1]] // Note: dp[i], not dp[i-1]!
);
}
}
}
return dp[n][amount] >= Integer.MAX_VALUE - 1 ? -1 : dp[n][amount];
}
Bounded (0/1): Each item used at most once
- Transition:
dp[i-1][w - cost]
(previous row)
Unbounded: Each item used unlimited times
- Transition:
dp[i][w - cost]
(same row)
Why same row? Because we can use the same coin multiple times!
7. Coin Change II (LC 518) - Unboundedβ
State Definition: dp[i][amount]
= number of ways to make amount
using first i
coins
Transition (Unbounded):
dp[i][amount] = dp[i-1][amount] // don't use coin i
+ dp[i][amount - coin[i]] // use coin i
public int change(int amount, int[] coins) {
int n = coins.length;
int[][] dp = new int[n + 1][amount + 1];
// Base case: 1 way to make amount 0 (use no coins)
for (int i = 0; i <= n; i++) {
dp[i][0] = 1;
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= amount; j++) {
dp[i][j] = dp[i-1][j]; // don't use coin i
if (j >= coins[i-1]) {
dp[i][j] += dp[i][j - coins[i-1]]; // use coin i
}
}
}
return dp[n][amount];
}
- Coin Change: Minimize count (use
min
) - Coin Change II: Count ways (use
+
)
Same structure, different operation!
8. Perfect Squares (LC 279)β
State Definition: dp[n]
= minimum number of perfect squares that sum to n
Transition:
dp[n] = Math.min(dp[n], 1 + dp[n - i*i]) for all i where i*i <= n
public int numSquares(int n) {
int[] dp = new int[n + 1];
Arrays.fill(dp, Integer.MAX_VALUE);
dp[0] = 0;
for (int i = 1; i <= n; i++) {
for (int j = 1; j * j <= i; j++) {
dp[i] = Math.min(dp[i], 1 + dp[i - j*j]);
}
}
return dp[n];
}
Items = perfect squares (1, 4, 9, 16, ...)
Capacity = target number n
Goal = minimize count
Each square can be used unlimited times!
9. Minimum Cost For Tickets (LC 983)β
State Definition: dp[i]
= minimum cost to cover all travel days up to day i
Transition: Try all ticket types (1-day, 7-day, 30-day)
public int mincostTickets(int[] days, int[] costs) {
int lastDay = days[days.length - 1];
boolean[] isTravelDay = new boolean[lastDay + 1];
for (int day : days) {
isTravelDay[day] = true;
}
int[] dp = new int[lastDay + 1];
for (int i = 1; i <= lastDay; i++) {
if (!isTravelDay[i]) {
dp[i] = dp[i-1]; // not traveling, no cost
continue;
}
// Option 1: 1-day pass
int cost1 = dp[i-1] + costs[0];
// Option 2: 7-day pass
int day7 = Math.max(0, i - 7);
int cost7 = dp[day7] + costs[1];
// Option 3: 30-day pass
int day30 = Math.max(0, i - 30);
int cost30 = dp[day30] + costs[2];
dp[i] = Math.min(cost1, Math.min(cost7, cost30));
}
return dp[lastDay];
}
10. Combination Sum IV (LC 377) - Order Mattersβ
State Definition: dp[amount]
= number of ways to make amount
(order matters!)
Transition:
dp[amount] = sum(dp[amount - num]) for all num in nums where num <= amount
public int combinationSum4(int[] nums, int target) {
int[] dp = new int[target + 1];
dp[0] = 1;
for (int i = 1; i <= target; i++) {
for (int num : nums) {
if (i >= num) {
dp[i] += dp[i - num];
}
}
}
return dp[target];
}
Coin Change II: Combinations (order doesn't matter)
- Loop coins first:
for coin: for amount
Combination Sum IV: Permutations (order matters)
- Loop amount first:
for amount: for nums
Different loop order β different counting!
Category 3: Subset Selectionβ
11. Partition to K Equal Sum Subsets (LC 698)β
State Definition: dp[mask][current_sum]
= true if we can partition items indicated by mask
to reach target
Transition: Include/exclude each number
When tracking "which items used", use bitmask to represent subsets.
This is advanced knapsack with subset tracking.
12. Tallest Billboard (LC 956)β
State Definition: dp[i][diff]
= maximum height when difference between two piles is diff
using first i
rods
Transition: Each rod can go to:
- Left pile
- Right pile
- Not used
This requires tracking difference between two subsets instead of just one sum.
Pattern Extension: Two knapsacks being filled simultaneously.
13. Shopping Offers (LC 638)β
State Definition: dp[needs]
= minimum cost to fulfill needs
(a shopping cart)
Transition: Either buy individual items or use a special offer
14. Number of Dice Rolls With Target Sum (LC 1155)β
State Definition: dp[i][sum]
= number of ways to get sum
using i
dice
Transition:
dp[i][sum] = sum(dp[i-1][sum - face]) for face in 1..k
public int numRollsToTarget(int n, int k, int target) {
int MOD = 1_000_000_007;
int[][] dp = new int[n + 1][target + 1];
dp[0][0] = 1;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= target; j++) {
for (int face = 1; face <= k; face++) {
if (j >= face) {
dp[i][j] = (dp[i][j] + dp[i-1][j - face]) % MOD;
}
}
}
}
return dp[n][target];
}
Items = dice (each can show 1 to k) Resource = target sum Count = number of dice used
Similar to coin change but counting dice instead of minimizing coins.
15. Profitable Schemes (LC 879)β
State Definition: dp[i][n][profit]
= number of ways to get at least profit
with at most n
people using first i
crimes
Transition:
dp[i][n][profit] = dp[i-1][n][profit]
+ dp[i-1][n - people[i]][max(0, profit - profit[i])]
Category 4: String/Sequence Variantsβ
16. Minimum ASCII Delete Sum for Two Strings (LC 712)β
State Definition: dp[i][j]
= minimum ASCII delete sum to make first i
characters of s1
equal to first j
characters of s2
Transition:
if (s1[i-1] == s2[j-1]) {
dp[i][j] = dp[i-1][j-1]; // no deletion needed
} else {
dp[i][j] = Math.min(
dp[i-1][j] + s1[i-1], // delete from s1
dp[i][j-1] + s2[j-1] // delete from s2
);
}
This is knapsack thinking applied to string matching:
- Items = characters
- Resource = positions in both strings
- Cost = ASCII values
17. Form Largest Integer With Digits That Add up to Target (LC 1449)β
State Definition: dp[target]
= maximum number of digits achievable with cost exactly target
Transition: For each cost value, try using it
18. Count Ways to Build Good Strings (LC 2466)β
State Definition: dp[length]
= number of ways to build string of length
Transition: Add zero
zeros or one
ones (bounded by counts)
19. Maximum Value of K Coins From Piles (LC 2218)β
State Definition: dp[i][k]
= maximum value from first i
piles taking k
coins total
Transition: For pile i
, try taking 0, 1, 2, ..., min(k, pile[i].length)
coins
public int maxValueOfCoins(List<List<Integer>> piles, int k) {
int n = piles.size();
int[][] dp = new int[n + 1][k + 1];
for (int i = 1; i <= n; i++) {
for (int coins = 0; coins <= k; coins++) {
int currentPileSum = 0;
// Try taking 0, 1, 2, ..., min(coins, pile size) from pile i
for (int take = 0; take <= Math.min(coins, piles.get(i-1).size()); take++) {
if (take > 0) {
currentPileSum += piles.get(i-1).get(take - 1);
}
dp[i][coins] = Math.max(
dp[i][coins],
dp[i-1][coins - take] + currentPileSum
);
}
}
}
return dp[n][k];
}
Instead of binary choice (take/skip), we have multiple choices: how many to take from this pile?
Pattern Extension: Bounded knapsack with quantity limits per item.
20. Minimum Difficulty of a Job Schedule (LC 1335)β
State Definition: dp[i][d]
= minimum difficulty for first i
jobs in d
days
Transition: Partition jobsβeach partition's difficulty = max job difficulty in that day
public int minDifficulty(int[] jobDifficulty, int d) {
int n = jobDifficulty.length;
if (n < d) return -1;
int[][] dp = new int[n][d + 1];
for (int i = 0; i < n; i++) {
Arrays.fill(dp[i], Integer.MAX_VALUE);
}
// Base case: all jobs in day 1
dp[0][1] = jobDifficulty[0];
for (int i = 1; i < n; i++) {
dp[i][1] = Math.max(dp[i-1][1], jobDifficulty[i]);
}
for (int day = 2; day <= d; day++) {
for (int i = day - 1; i < n; i++) {
// Try all possible splits
for (int j = i; j >= day - 1; j--) {
int maxDifficulty = 0;
for (int k = j; k <= i; k++) {
maxDifficulty = Math.max(maxDifficulty, jobDifficulty[k]);
}
if (dp[j-1][day-1] != Integer.MAX_VALUE) {
dp[i][day] = Math.min(
dp[i][day],
dp[j-1][day-1] + maxDifficulty
);
}
}
}
}
return dp[n-1][d];
}
The Common Threadβ
Every problem has:
- Two dimensions:
(items processed, resource remaining/used)
- Resource constraint: weight, capacity, sum, count, budget
- Binary/Multiple choice: take/skip, or how many times to take
- Recurrence:
dp[i][resource] = best(skip, take(s))
- Goal: Optimize (max/min) or count ways under constraint
Key Insight: You're allocating a limited resource across items. The constraint is what makes it DPβwithout it, greedy might work!
Classic Loop Structureβ
// Bounded (0/1) Knapsack
for (int i = 1; i <= n; i++) { // items
for (int w = 1; w <= capacity; w++) { // resource
dp[i][w] = Math.max(
dp[i-1][w], // skip item i
dp[i-1][w-cost] + value // take item i (previous row!)
);
}
}
// Unbounded Knapsack
for (int i = 1; i <= n; i++) { // items
for (int w = 1; w <= capacity; w++) { // resource
dp[i][w] = Math.max(
dp[i-1][w], // skip item i
dp[i][w-cost] + value // take item i (same row!)
);
}
}
Bounded vs Unboundedβ
Bounded (0/1): Each item used at most once
for (int i = 1; i <= n; i++) {
for (int w = capacity; w >= cost[i]; w--) { // reverse order!
dp[w] = Math.max(dp[w], dp[w - cost[i]] + value[i]);
}
}
Unbounded: Each item used unlimited times
for (int i = 1; i <= n; i++) {
for (int w = cost[i]; w <= capacity; w++) { // forward order!
dp[w] = Math.max(dp[w], dp[w - cost[i]] + value[i]);
}
}
Key Difference:
- Bounded:
dp[i-1][...]
(previous row) - Unbounded:
dp[i][...]
(same rowβcan reuse!)
Pattern Recognition Checklistβ
When you see a new problem, ask:
- Do I have a collection of items to choose from?
- Is there a limited resource (capacity, budget, sum, count)?
- Does each item have a cost (weight, size, value)?
- Do I need to optimize (max/min) or count ways?
- Can I define
dp[i][resource]
as "best answer using first i items with resource constraint"? - Does each item have a choice: take or skip?
If all YES β Pattern 3: Bounded Knapsack
Common Mistakesβ
Mistake 1: Bounded vs Unbounded confusionβ
// WRONG - using unbounded logic for bounded problem
dp[i][w] = Math.max(dp[i][w], dp[i][w-cost] + value);
// RIGHT - bounded uses previous row
dp[i][w] = Math.max(dp[i][w], dp[i-1][w-cost] + value);
Mistake 2: Not checking if item fitsβ
// WRONG - may access negative index
dp[i][w] = dp[i-1][w-cost] + value;
// RIGHT - check constraint first
if (w >= cost) {
dp[i][w] = Math.max(dp[i][w], dp[i-1][w-cost] + value);
}
Mistake 3: Base case initializationβ
// For "minimum" problems
Arrays.fill(dp[0], Integer.MAX_VALUE);
dp[0][0] = 0;
// For "count ways" problems
dp[0][0] = 1; // one way to make sum 0
// For "can we make" problems
dp[0][0] = true;
Space Optimizationβ
Observation: Each row only depends on the previous row.
2D β 1D (Bounded):
// Before: O(n Γ W)
int[][] dp = new int[n + 1][W + 1];
for (int i = 1; i <= n; i++) {
for (int w = W; w >= weight[i]; w--) { // REVERSE!
dp[i][w] = Math.max(
dp[i-1][w],
dp[i-1][w - weight[i]] + value[i]
);
}
}
// After: O(W)
int[] dp = new int[W + 1];
for (int i = 1; i <= n; i++) {
for (int w = W; w >= weight[i]; w--) { // REVERSE!
dp[w] = Math.max(
dp[w], // old dp[w] = dp[i-1][w]
dp[w - weight[i]] + value[i] // old value = dp[i-1][w-weight]
);
}
}
Why reverse? To avoid overwriting values we still need!
Learning Pathβ
Week 1: Core Knapsack (Problems 1-5)
- Master classic 0/1 Knapsack
- Learn Partition/Subset Sum pattern
- Understand bounded vs unbounded
Week 2: Coin Change (Problems 6-10)
- Practice unbounded knapsack
- Learn counting vs optimization
- Master order-dependent variations
Week 3: Subset Selection (Problems 11-15)
- Advanced state tracking (bitmask)
- Multiple knapsacks
- Complex constraints
Week 4: String/Sequence (Problems 16-20)
- Apply knapsack to strings
- Multiple choices per item
- Job scheduling patterns
Total: 20 problems Γ 4 weeks = Pattern 3 mastery
Quick Reference Tableβ
Problem | State | Resource | Operation | Pattern Type |
---|---|---|---|---|
0/1 Knapsack | max value with i items, w capacity | weight | max(skip, take) | Classic |
Partition Sum | can make sum with i items | sum | OR(skip, take) | Boolean |
Target Sum | ways to reach sum with i items | sum | +(skip +, skip -) | Counting |
Coin Change | min coins for amount with i coins | amount | min(skip, take) | Unbounded Min |
Coin Change II | ways for amount with i coins | amount | +(skip, take) | Unbounded Count |
Ones/Zeroes | max subset with i items, m 0s, n 1s | zeros, ones | max(skip, take) | 2D Resource |
Dice Rolls | ways to get sum with i dice | sum, dice | sum(all faces) | Bounded Count |
Max Coins | max value from i piles, k coins | piles, coins | max(take 0..n) | Multi-choice |
Detection Pattern Summaryβ
See these keywords β Think Knapsack:
- "Subset" + "sum" β Partition/Target Sum variant
- "Coins" + "amount" β Coin Change variant
- "Capacity" + "weight" + "value" β Classic Knapsack
- "At most" + "budget/limit" β Resource constraint
- "Ways to" + "target/sum" β Counting variant
- "Minimum/Maximum" + "select items" β Optimization variant
All lead to: dp[items][resource] = best(skip, take(s))
Next Stepsβ
You're ready for:
- Pattern 4: Interval DP (range-based problems)
- Pattern 5: State Machine DP (mode transitions)
- Advanced Knapsack (multi-dimensional constraints)
Connection to Previous Patterns:
- Pattern 1: 1D state (position only)
- Pattern 2: 2D state (two positions)
- Pattern 3: 2D state (items + resource) β New dimension type!
Final Thoughtsβ
Bounded Knapsack is the most powerful optimization pattern in DP.
Master these 20 problems, and you'll:
- Recognize "this is a knapsack problem" instantly
- Know when to use bounded vs unbounded
- Transform problems into knapsack formulation
- Handle multiple resource constraints
- See connections to subset, partition, coin problems
This is your resource allocation foundation. Build it strong.
Summary: The Knapsack Templateβ
// Classic 0/1 Knapsack Template
public int knapsack(int[] items, int[] costs, int capacity) {
int n = items.length;
int[][] dp = new int[n + 1][capacity + 1];
for (int i = 1; i <= n; i++) {
for (int w = 1; w <= capacity; w++) {
// Always have option to skip
dp[i][w] = dp[i-1][w];
// Take if it fits
if (costs[i-1] <= w) {
dp[i][w] = Math.max(
dp[i][w],
dp[i-1][w - costs[i-1]] + items[i-1]
);
}
}
}
return dp[n][capacity];
}
"For each item, try skipping it or taking it (if resource allows). Build the answer from smaller subproblems."
This is the essence of knapsack DP.