Structs & Data Structures · intermediate · ~15 min
Choose items under a capacity, each at most once.
The 0/1 knapsack maximises the value you can carry within a weight capacity when each item may be taken at most once. The textbook table is 2-D — items × capacity — but it collapses to a single array indexed by capacity, and that collapse comes with the most important rule in this track: iterate capacity downward. Going upward would let the same item be picked twice (that is the unbounded knapsack, and exactly what coin change wanted). Subset-sum is the same algorithm with booleans instead of values: can any subset hit this target?
Knapsack is the archetype for constrained selection — budget allocation, cargo loading, choosing which jobs to run under a resource cap, selecting features under a size limit. And the downward-iteration rule is the kind of one-line detail that silently produces plausible-but-wrong answers: the code runs, the numbers look reasonable, and items have been used twice.
The recurrence. For item i with weight w and value v: dp[cap] = max(dp[cap], dp[cap - w] + v). Take it or leave it — the best of the two.
Why downward. In the 1-D array, dp[cap - w] must refer to the state before item i was considered. Iterating capacity from high to low guarantees that, because the smaller index has not been updated yet in this pass. Iterating upward means dp[cap-w] may already include item i, so it gets taken again — silently turning 0/1 into unbounded.
Unbounded on purpose. If items may repeat, iterate upward. The direction is the only difference between the two problems, which is worth internalising.
Subset sum. Replace values with reachability: dp[s] = dp[s] || dp[s - a[i]], again descending. dp[0] = 1 because the empty subset always reaches zero.
Space. The 1-D form is O(capacity) memory instead of O(items × capacity) — a large practical difference — but it discards the information needed to reconstruct which items were chosen. Keep the 2-D table if you need the selection, not just the score.
Complexity. O(n × capacity) time. That is pseudo-polynomial: it scales with the numeric capacity, not with the input's bit length, so a huge capacity is expensive even with few items.
#include <stdlib.h>
int knapsack(const int *w, const int *v, int n, int cap) {
int *dp = calloc((size_t)cap + 1, sizeof *dp); // dp[c] = best value within capacity c
if (!dp) return -1;
for (int i = 0; i < n; i++)
for (int c = cap; c >= w[i]; c--) // DOWNWARD: each item used at most once
if (dp[c - w[i]] + v[i] > dp[c])
dp[c] = dp[c - w[i]] + v[i];
int r = dp[cap];
free(dp);
return r;
}
/* subset sum: same shape, reachability instead of value */
int subset_sum(const int *a, int n, int t) {
char *dp = calloc((size_t)t + 1, 1);
if (!dp) return -1;
dp[0] = 1; // empty subset reaches 0
for (int i = 0; i < n; i++)
for (int s = t; s >= a[i]; s--) // DOWNWARD again
if (dp[s - a[i]]) dp[s] = 1;
int r = dp[t];
free(dp);
return r;
}
Key points:
c >= w[i] as the loop bound removes any need for a separate negative-index check.calloc matters: an unvisited capacity must start at 0 (or false).The 0/1 knapsack maximizes value under a weight budget with each item usable once; subset sum is its boolean cousin. Both use a 1-D table swept downward over capacity — that downward direction is exactly what prevents reusing an item within the same pass.
#include <stdio.h>
#include <stdlib.h>
static int knapsack(const int *w,const int *v,int n,int cap){ int *dp=calloc(cap+1,sizeof(int)); for(int i=0;i<n;i++)for(int c=cap;c>=w[i];c--)if(dp[c-w[i]]+v[i]>dp[c])dp[c]=dp[c-w[i]]+v[i]; int r=dp[cap]; free(dp); return r; }
static int subset_sum(const int *a,int n,int t){ char *dp=calloc(t+1,1); dp[0]=1; for(int i=0;i<n;i++)for(int s=t;s>=a[i];s--)if(dp[s-a[i]])dp[s]=1; int r=dp[t]; free(dp); return r; }
int main(void){
int w[]={1,3,4,5}, v[]={1,4,5,7};
printf("max value in a knapsack of capacity 7 = %d\n", knapsack(w,v,4,7));
int a[]={3,34,4,12,5,2};
printf("subset summing to 9? %s\n", subset_sum(a,6,9)?"yes":"no");
return 0;
}
| Step | Line | What happens |
|---|---|---|
| 1 | calloc(cap+1) |
Every capacity starts at value 0 — carrying nothing is always possible. |
| 2 | outer for (i) |
One pass per item; after pass i, dp is the best answer using only items 0..i. |
| 3 | for (c = cap; c >= w[i]; c--) |
Descending. dp[c - w[i]] is still the previous pass's value, so item i cannot be reused. |
| 4 | dp[c-w[i]] + v[i] > dp[c] |
Compare taking the item against not taking it, keeping the better. |
| 5 | after all items | dp[cap] is the optimum. With w={2,3}, v={3,4}, cap=5 → 7. |
| 6 | ascending instead | The same loop upward would allow item 0 twice, reporting dp[4] = 6 from a single copy — the classic bug. |
Sweeping capacity upward (turns 0/1 into unbounded, reusing items).
Compiler errors and warnings:
warning: implicit conversion loses integer precision when mixing int and size_t at the allocation; cast explicitly.Runtime symptoms:
for (c = cap; c >= w[i]; c--).w[i], indexing negatively — use c >= w[i] as the condition.dp[0] = 1 missing in subset-sum, or values/weights swapped in the call.Technique: test with a single item that fits twice — w={2}, v={3}, cap=5. The 0/1 answer is 3; if you get 6 or 7, your loop runs the wrong way.
(size_t)cap + 1 — validate that cap >= 0 first. A negative capacity casts to an enormous size_t and the allocation either fails or succeeds at a size you did not intend.calloc can fail; the code above returns -1 rather than dereferencing NULL.calloc (not malloc) is correct here.return dp[cap] after freeing.c >= w[i] invariant and can cause an infinite-looking loop or a negative index. Reject them at the boundary.int for large inputs; widen the accumulator if the data warrants it.Concrete uses: Cargo and container loading. Selecting which advertisements fit a slot budget. Choosing which features fit a firmware size limit. Portfolio selection under a spend cap. Cutting-stock and resource-allocation planning. Subset-sum underlies partition problems and appears in cryptanalysis of knapsack-based ciphers.
Professional best practices:
Beginner:
Intermediate:
1. (Beginner) 0/1 knapsack. Implement int knapsack(const int *w, const int *v, int n, int cap) with the 1-D descending loop. Example: w={2,3}, v={3,4}, cap=5 → 7. Concepts: take-or-leave recurrence, loop direction.
2. (Beginner) Prove the direction matters. Run the same input with an ascending inner loop and show the inflated result. Example: w={2}, v={3}, cap=5 → 3 descending, 6 ascending. Concepts: 0/1 vs unbounded.
3. (Intermediate) Subset sum. Implement int subset_sum(const int *a, int n, int t). Example: {3,34,4,12,5,2}, target 9 → 1 (4+5); target 30 → 0. Concepts: reachability DP.
4. (Intermediate) Reconstruct the chosen items. Use a 2-D table to report which items make the optimum, not just its value. Hint: walk backwards comparing dp[i][c] with dp[i-1][c]. Concepts: DP reconstruction, the space/information trade-off.
The 0/1 knapsack maximises value under a capacity with each item used at most once: dp[cap] = max(dp[cap], dp[cap-w] + v), collapsed to a single capacity-indexed array. The rule that makes it correct is iterating capacity downward, so dp[cap-w] still refers to the state before the current item — iterate upward and you have written the unbounded knapsack instead, silently reusing items. Subset-sum is the same loop with booleans and dp[0] = 1. The 1-D form gives the score in O(capacity) space; keep the 2-D table when you need to know which items were chosen.