Senior 13 min · March 05, 2026

0/1 Knapsack — The 1D Iteration Bug That Costs Months

Wrong iteration direction caused 40% cloud over-provisioning for 3 months with zero errors.

N
Naren Founder & Principal Engineer

20+ years shipping performance-critical code where algorithms decide the bill. Drawn from code that ran under real load.

Follow
Production
production tested
May 23, 2026
last updated
1,554
articles · all by Naren
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • 0/1 Knapsack: choose items to maximize value within a weight limit, each item taken at most once
  • dp[i][w] = max value using first i items with capacity w
  • Recurrence: max(skip: dp[i-1][w], take: val[i] + dp[i-1][w - wt[i]])
  • 1D optimization reduces space to O(W) but you MUST iterate capacity RIGHT-TO-LEFT — not optional
  • Forward iteration in 1D silently becomes unbounded knapsack — no crash, no exception, just wrong answers that look plausible
  • O(n*W) is pseudo-polynomial: fast when W is small, completely infeasible when W exceeds roughly 10^7 with a 2D table
  • The biggest production mistake: leaving the iteration direction to guesswork. Add a comment and a unit test.
✦ Definition~90s read
What is 0/1 Knapsack Problem?

The 0/1 Knapsack problem: given n items each with a weight and a value, and a knapsack with a fixed weight capacity W, choose a subset of items that fits within W and maximizes total value. The 0/1 constraint means each item can be taken at most once — you either include it fully or exclude it entirely. There is no fractional option.

Imagine you are going trick-or-treating with a bag that can only hold 10 pounds of candy.

Real-world framing: you are packing for a camping trip with a bag that holds 10 kg. Each piece of gear has a weight and a usefulness score. You want to maximize usefulness without overloading the bag. The catch is that taking the single heaviest most-useful item might block you from taking three lighter items whose combined usefulness is higher.

Greedy selection by value-to-weight ratio does not work here — that is fractional knapsack, a different problem.

The brute-force approach tries every possible subset — all 2^n combinations. For n=20 that is one million subsets, manageable. For n=40 that is over one trillion — completely infeasible. Dynamic Programming reduces this to O(n × W) by observing that the same subproblem — 'what is the best I can do with items 1 through k and capacity c?' — appears hundreds of times in the recursive tree.

DP solves each unique subproblem exactly once and stores the result.

The key insight that makes DP work here is optimal substructure: the optimal solution to the full problem contains optimal solutions to its subproblems. If you decided optimally on item k, the remaining items 1 through k-1 filling the remaining capacity must also be chosen optimally.

This property, combined with overlapping subproblems, is what separates problems where DP works from problems where it does not.

Plain-English First

Imagine you are going trick-or-treating with a bag that can only hold 10 pounds of candy. Every house offers different candy — some heavy, some light, some incredibly delicious. You cannot cut a candy bar in half; you either take the whole thing or leave it behind. Your job is to pick the combination that makes your bag as valuable as possible without ripping the strap off. That is the 0/1 Knapsack Problem — choosing whole items under a fixed capacity to maximize total value. The twist that makes it hard is that taking one great item might block you from taking two good items that together would have been better.

Resource allocation is one of the most common problems in software engineering, and it shows up in more places than most engineers expect. Budget planning tools, cloud VM packing algorithms, cargo loading systems, ad auction bidding engines, compiler register allocation — all of these are flavors of the same underlying constraint: given a fixed budget of some resource, which items do you select to extract maximum value? The 0/1 Knapsack Problem is the canonical formulation of this class of challenge, and mastering it gives you a reusable mental template for an entire family of optimization problems.

The naive recursive approach explodes into O(2^n) time because every item forces a binary branch — take it or leave it — and those branches multiply exponentially. With 40 items that is over a trillion subsets. Dynamic Programming tames this by recognizing that subproblems overlap: the best way to fill a 7 kg bag using items 1 through 4 gets recomputed dozens of times in the recursive tree. DP caches each unique subproblem result so it is solved exactly once, collapsing exponential work into a pseudo-polynomial O(n × W) solution.

By the end of this article you will understand not just the DP table mechanics but why each cell means what it means, how to reconstruct which items were actually chosen rather than just the maximum value, how to reduce space from O(n × W) to O(W) without losing correctness, and the subtle iteration direction bug that causes silent wrong answers in production optimizers. You will also be ready for the four hardest interview questions that trip up engineers who have memorized the algorithm without understanding it.

What is 0/1 Knapsack — Plain English Before the Math

The 0/1 Knapsack problem: given n items each with a weight and a value, and a knapsack with a fixed weight capacity W, choose a subset of items that fits within W and maximizes total value. The 0/1 constraint means each item can be taken at most once — you either include it fully or exclude it entirely. There is no fractional option.

Real-world framing: you are packing for a camping trip with a bag that holds 10 kg. Each piece of gear has a weight and a usefulness score. You want to maximize usefulness without overloading the bag. The catch is that taking the single heaviest most-useful item might block you from taking three lighter items whose combined usefulness is higher. Greedy selection by value-to-weight ratio does not work here — that is fractional knapsack, a different problem.

The brute-force approach tries every possible subset — all 2^n combinations. For n=20 that is one million subsets, manageable. For n=40 that is over one trillion — completely infeasible. Dynamic Programming reduces this to O(n × W) by observing that the same subproblem — 'what is the best I can do with items 1 through k and capacity c?' — appears hundreds of times in the recursive tree. DP solves each unique subproblem exactly once and stores the result.

The key insight that makes DP work here is optimal substructure: the optimal solution to the full problem contains optimal solutions to its subproblems. If you decided optimally on item k, the remaining items 1 through k-1 filling the remaining capacity must also be chosen optimally. This property, combined with overlapping subproblems, is what separates problems where DP works from problems where it does not.

The DP Table as a Decision Grid
  • Row i represents the universe of available items — items 1 through i exist; items i+1 through n do not.
  • Column w represents the remaining weight budget — how much capacity is available for this decision.
  • Each cell inherits the best result from the row above (skip item i) or combines item i's value with the best result from a smaller capacity (take item i).
  • The answer accumulates at the bottom-right corner: dp[n][W] — all items considered, full capacity available.
  • The table is a complete record of all optimal sub-solutions. Every cell is correct given its row and column constraints. You never need to recompute anything.
Production Insight
Brute-force 2^n is infeasible for n greater than roughly 40 — that is over one trillion subsets evaluated.
DP reduces this to O(n × W), which is feasible when W is bounded — typically under 10^7 for the 2D version.
Rule: if n exceeds 40 and W is reasonable, DP is your only viable exact algorithm. If W is also large, you need approximation algorithms or branch-and-bound with aggressive pruning.
Key Takeaway
0/1 means binary choice per item — take it fully or skip it entirely, no fractions.
Brute force is exponential in n. DP makes it pseudo-polynomial in both n and W.
The feasibility of DP is determined by W, not n — this distinction matters when inputs scale.
0/1 Knapsack: 1D Iteration Direction Bug THECODEFORGE.IO 0/1 Knapsack: 1D Iteration Direction Bug Why reversing the inner loop is critical for correctness 0/1 Knapsack Problem Max value with weight ≤ capacity, each item once 2D DP Table dp[i][w] = max(dp[i-1][w], val[i] + dp[i-1][w-wt[i]]) 1D Space Optimization Reduce to dp[w] by reusing a single array Reverse Iteration Loop w from capacity down to weight[i] Correct DP Update dp[w] = max(dp[w], val[i] + dp[w - wt[i]]) ⚠ Forward iteration reuses updated values, allowing multiple uses Always iterate capacity descending to enforce 0/1 constraint THECODEFORGE.IO
thecodeforge.io
0/1 Knapsack: 1D Iteration Direction Bug
01 Knapsack Problem

How 0/1 Knapsack Works — The Recurrence Built from First Principles

Rather than memorizing the recurrence, derive it from the decision you face at each cell.

Define dp[i][w] as the maximum value achievable using only items 1 through i with weight capacity exactly w available.

Base cases are immediate: dp[0][w] = 0 for all w — with zero items, no value is possible regardless of capacity. dp[i][0] = 0 for all i — with zero capacity, nothing can fit regardless of how many items exist.

For any other cell dp[i][w], you face exactly one decision about item i:

Option A — Skip item i: you decide not to include item i. The best you can do is whatever was optimal for items 1 through i-1 with the same capacity w. That value is already computed: dp[i-1][w].

Option B — Take item i: this is only possible if item i's weight wt[i] does not exceed current capacity w. If you take item i, you gain val[i] and you have w - wt[i] capacity remaining for items 1 through i-1. The best outcome for the remaining capacity is dp[i-1][w - wt[i]], already computed.

The recurrence combines both options: If wt[i] > w: dp[i][w] = dp[i-1][w] (cannot take item i) If wt[i] <= w: dp[i][w] = max(dp[i-1][w], val[i] + dp[i-1][w - wt[i]])

The answer after considering all n items with full capacity W is dp[n][W].

Time complexity: O(n × W) — n items, W+1 capacity values, one O(1) computation per cell. Space complexity: O(n × W) for the full 2D table, or O(W) with the 1D rolling array optimization.

The Two-Question Rule for Every Cell
For each cell dp[i][w], ask exactly two questions in sequence: 1. Can item i physically fit? Is wt[i] <= w? If no, the answer is dp[i-1][w] — inherit from above, no further thought needed. 2. If it can fit, is taking it better than skipping it? Compare val[i] + dp[i-1][w - wt[i]] against dp[i-1][w]. Take the maximum. Every cell in the entire table follows this identical two-question process. The algorithm has no special cases beyond the base row and column. Once you internalize these two questions, you can reconstruct the recurrence from memory under interview pressure without ever having to 'memorize' it.
Production Insight
The recurrence references dp[i-1][...] for both options — the previous row, not the current one.
This is precisely why left-to-right iteration in the 1D version breaks correctness: when you update dp[w] and then use dp[w - wt[i]] later in the same pass, you are referencing an already-updated value rather than the previous row's value.
Rule: when in doubt about whether your DP references the right state, draw out two rows of the table by hand and trace which cells the recurrence reads from for three consecutive items.
Key Takeaway
The recurrence is binary: skip (copy from row above) or take (add value plus look up the remainder row).
Base cases at row 0 and column 0 are both zero — this is what justifies starting the iteration at i=1 and w=1.
The entire algorithm reduces to one decision per cell: dp[i][w] = max(skip, take), with take only valid when the item fits.

Worked Example — Tracing the Full DP Table by Hand

Items: item 1 (weight=2, value=6), item 2 (weight=2, value=10), item 3 (weight=3, value=12). Capacity W=5.

This example is small enough to trace completely but large enough to show all interesting decisions — specifically the case at w=4 in row 3 where skipping yields a better result than taking, and w=5 in row 3 where taking produces the global optimum.

Initial table — row 0 (base case, no items): w=0 w=1 w=2 w=3 w=4 w=5 i=0: [ 0, 0, 0, 0, 0, 0 ]

Row i=1 (item 1: weight=2, value=6): w=0: wt[1]=2 > 0. Skip. dp[1][0] = dp[0][0] = 0. w=1: wt[1]=2 > 1. Skip. dp[1][1] = dp[0][1] = 0. w=2: wt[1]=2 <= 2. Take? 6 + dp[0][0] = 6. Skip? dp[0][2] = 0. Max = 6. dp[1][2] = 6. w=3: wt[1]=2 <= 3. Take? 6 + dp[0][1] = 6. Skip? dp[0][3] = 0. Max = 6. dp[1][3] = 6. w=4: wt[1]=2 <= 4. Take? 6 + dp[0][2] = 6. Skip? dp[0][4] = 0. Max = 6. dp[1][4] = 6. w=5: wt[1]=2 <= 5. Take? 6 + dp[0][3] = 6. Skip? dp[0][5] = 0. Max = 6. dp[1][5] = 6. i=1: [ 0, 0, 6, 6, 6, 6 ]

Row i=2 (item 2: weight=2, value=10): w=0,1: wt[2]=2 > w. Skip. dp[2][0]=0, dp[2][1]=0. w=2: Take? 10 + dp[1][0] = 10. Skip? dp[1][2] = 6. Max = 10. dp[2][2] = 10. w=3: Take? 10 + dp[1][1] = 10. Skip? dp[1][3] = 6. Max = 10. dp[2][3] = 10. w=4: Take? 10 + dp[1][2] = 10 + 6 = 16. Skip? dp[1][4] = 6. Max = 16. dp[2][4] = 16. (Taking both item 1 and item 2 uses exactly weight 4.) w=5: Take? 10 + dp[1][3] = 10 + 6 = 16. Skip? dp[1][5] = 6. Max = 16. dp[2][5] = 16. i=2: [ 0, 0, 10, 10, 16, 16 ]

Row i=3 (item 3: weight=3, value=12): w=0,1,2: wt[3]=3 > w. Skip. dp[3][0]=0, dp[3][1]=0, dp[3][2]=10. w=3: Take? 12 + dp[2][0] = 12. Skip? dp[2][3] = 10. Max = 12. dp[3][3] = 12. w=4: Take? 12 + dp[2][1] = 12 + 0 = 12. Skip? dp[2][4] = 16. Max = 16. dp[3][4] = 16. (Skipping is better here — items 1 and 2 together are worth more than item 3 alone.) w=5: Take? 12 + dp[2][2] = 12 + 10 = 22. Skip? dp[2][5] = 16. Max = 22. dp[3][5] = 22. (Taking item 3 and item 2 together: weight 5, value 22 — the global optimum.) i=3: [ 0, 0, 10, 12, 16, 22 ]

Answer: dp[3][5] = 22. Items selected: item 2 (value=10, weight=2) and item 3 (value=12, weight=3). Total weight=5=W. Total value=22.

Notice that item 1 (value=6, weight=2) was not selected — item 2 is strictly better with the same weight and higher value, so item 1 never participates in the optimal solution for any capacity where item 2 fits.

io/thecodeforge/dp/example_trace.txtTEXT
1
See the DP table above
Output
dp[3][5] = 22
Why This Example Matters
The decision at w=4 in row 3 is critical: skipping item 3 (value 12) allows items 1 and 2 (value 16) to fit. This is the non-greedy property that forces us to consider both options. If your DP implementation always takes the item when it fits, it would return 12 and be wrong.
Production Insight
Tracing the table by hand catches indexing errors that unit tests frequently miss — especially off-by-one errors in the wt[i-1] vs wt[i] mapping between 1-indexed DP rows and 0-indexed weight arrays.
Always verify your implementation against at least one hand-traced example where the optimal solution is not simply 'take everything that fits' — the row i=3, w=4 decision in this example (where skipping wins) is exactly the kind of case that exposes incorrect recurrence implementations.
Rule: if your hand trace and your code disagree on even one cell, the code is wrong — re-examine the array indexing and the weight guard condition before anything else.
Key Takeaway
Hand-tracing a small example exposes off-by-one indexing errors that survive automated testing when test cases happen to use items that all fit.
The final answer is always dp[n][W] — bottom-right corner of the full table.
Pay particular attention to cells where skipping wins — these are the cells that distinguish correct implementations from ones that always take items greedily.

Complete Implementation — 2D Table and 1D Optimization with Item Reconstruction

The implementation below provides three things: the 2D table version that enables item reconstruction, the 1D space-optimized version for memory-constrained environments, and the backtracking function that reconstructs which items were actually selected from the 2D table. All three are production-quality with input validation and long arithmetic throughout.

io/thecodeforge/dp/KnapsackSolver.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
package io.thecodeforge.dp;

import java.util.ArrayList;
import java.util.List;

/**
 * Production-grade 0/1 Knapsack solver.
 *
 * Two implementations:
 *   solve2D  — full (n+1) x (W+1) table. Enables item reconstruction.
 *              Use when you need to know WHICH items were selected.
 *
 *   solve1D  — single rolling array of size (W+1). O(W) space.
 *              Use only when you need the max value and memory is tight.
 *              Cannot reconstruct which items were chosen.
 *
 * Both use long arithmetic throughout to prevent silent integer overflow
 * when individual item values or their sum exceeds Integer.MAX_VALUE.
 */
public class KnapsackSolver {

    // ----------------------------------------------------------------
    // 2D DP — full table, O(n * W) space, enables reconstruction
    // ----------------------------------------------------------------

    /**
     * Solves 0/1 knapsack using a full 2D DP table.
     *
     * @param weights  item weights, 0-indexed, length n
     * @param values   item values,  0-indexed, length n (must match weights)
     * @param capacity maximum total weight the knapsack can hold (>= 0)
     * @return maximum achievable value without exceeding capacity
     * @throws IllegalArgumentException if inputs are null, mismatched, or capacity < 0
     */
    public static long solve2D(int[] weights, long[] values, int capacity) {
        validateInputs(weights, values, capacity);
        int n = weights.length;

        // (n+1) rows: row 0 = base case (no items)
        // (W+1) cols: col 0 = base case (zero capacity)
        // Java zero-initializes long arrays — base cases are already set
        long[][] dp = new long[n + 1][capacity + 1];

        for (int i = 1; i <= n; i++) {
            int  wt  = weights[i - 1];  // map 1-indexed row to 0-indexed array
            long val = values[i - 1];

            for (int w = 0; w <= capacity; w++) {
                // Option A: skip item i
                dp[i][w] = dp[i - 1][w];

                // Option B: take item i (only if it fits)
                if (wt <= w) {
                    long takeValue = val + dp[i - 1][w - wt];
                    if (takeValue > dp[i][w]) {
                        dp[i][w] = takeValue;
                    }
                }
            }
        }
        return dp[n][capacity];
    }

    /**
     * Reconstructs which items were selected in the optimal 0/1 knapsack solution.
     *
     * Backtracks through the 2D DP table starting at dp[n][W].
     * At each row i, if dp[i][w] != dp[i-1][w], item i was taken.
     *
     * @param weights  item weights (same array passed to solve2D)
     * @param values   item values  (same array passed to solve2D)
     * @param capacity knapsack capacity (same value passed to solve2D)
     * @return 0-indexed positions of items selected in the optimal solution
     */
    public static List<Integer> reconstruct(int[] weights, long[] values, int capacity) {
        validateInputs(weights, values, capacity);
        int n = weights.length;
        long[][] dp = new long[n + 1][capacity + 1];

        // Rebuild the table — identical to solve2D
        for (int i = 1; i <= n; i++) {
            int  wt  = weights[i - 1];
            long val = values[i - 1];
            for (int w = 0; w <= capacity; w++) {
                dp[i][w] = dp[i - 1][w];
                if (wt <= w) {
                    long takeValue = val + dp[i - 1][w - wt];
                    if (takeValue > dp[i][w]) {
                        dp[i][w] = takeValue;
                    }
                }
            }
        }

        // Backtrack from dp[n][capacity] to find selected items
        List<Integer> selected = new ArrayList<>();
        int w = capacity;
        for (int i = n; i >= 1; i--) {
            // If this row's value differs from the row above,
            // item i was taken — it changed the optimal value
            if (dp[i][w] != dp[i - 1][w]) {
                selected.add(0, i - 1);  // convert to 0-indexed, maintain order
                w -= weights[i - 1];     // reduce remaining capacity
            }
            // If equal, item i was skipped — move up without changing w
        }

        // Invariant: selected items must not exceed capacity
        int totalWeight = selected.stream().mapToInt(idx -> weights[idx]).sum();
        assert totalWeight <= capacity
            : "Reconstruction invariant violated: totalWeight=" + totalWeight
              + " exceeds capacity=" + capacity;

        return selected;
    }

    // ----------------------------------------------------------------
    // 1D space-optimized — O(W) space, RIGHT-TO-LEFT iteration
    // ----------------------------------------------------------------

    /**
     * Solves 0/1 knapsack using a single rolling DP array.
     *
     * CRITICAL: the inner loop iterates capacity RIGHT-TO-LEFT (high to low).
     * Left-to-right iteration silently converts this into unbounded knapsack,
     * allowing items to be selected multiple times. There is no runtime error.
     *
     * Use this variant only when:
     *   - You need only the maximum value (not which items were chosen), AND
     *   - Memory is a constraint (W > ~10^6 makes 2D infeasible).
     *
     * @param weights  item weights, 0-indexed
     * @param values   item values,  0-indexed
     * @param capacity maximum total weight
     * @return maximum achievable value
     */
    public static long solve1D(int[] weights, long[] values, int capacity) {
        validateInputs(weights, values, capacity);
        long[] dp = new long[capacity + 1];
        // dp[w] is initialized to 0 — represents 'no items considered yet'

        for (int i = 0; i < weights.length; i++) {
            int  wt  = weights[i];
            long val = values[i];

            // RIGHT-TO-LEFT: ensures dp[w - wt] still reflects the state
            // BEFORE item i was considered (previous row in the 2D table).
            //
            // If we iterated left-to-right:
            //   dp[wt] gets updated to include item i.
            //   Then dp[2*wt] reads the already-updated dp[wt] and adds item i again.
            //   Item i gets selected twice — unbounded knapsack, not 0/1.
            for (int w = capacity; w >= wt; w--) {
                long takeValue = val + dp[w - wt];
                if (takeValue > dp[w]) {
                    dp[w] = takeValue;
                }
            }
        }
        return dp[capacity];
    }

    // ----------------------------------------------------------------
    // Input validation — shared by all variants
    // ----------------------------------------------------------------

    private static void validateInputs(int[] weights, long[] values, int capacity) {
        if (weights == null || values == null) {
            throw new IllegalArgumentException("weights and values arrays must not be null");
        }
        if (weights.length != values.length) {
            throw new IllegalArgumentException(
                "weights and values must have the same length: "
                + "weights.length=" + weights.length
                + ", values.length=" + values.length);
        }
        if (capacity < 0) {
            throw new IllegalArgumentException(
                "capacity must be non-negative, got: " + capacity);
        }
        for (int i = 0; i < weights.length; i++) {
            if (weights[i] < 0) {
                throw new IllegalArgumentException(
                    "Item weight must be non-negative. weights[" + i + "]=" + weights[i]);
            }
            if (values[i] < 0) {
                throw new IllegalArgumentException(
                    "Item value must be non-negative. values[" + i + "]=" + values[i]);
            }
        }
    }

    // ----------------------------------------------------------------
    // Demo
    // ----------------------------------------------------------------

    public static void main(String[] args) {
        int[]  weights  = {2, 2, 3};
        long[] values   = {6L, 10L, 12L};
        int    capacity = 5;

        System.out.println("=== 0/1 Knapsack Demo ===");
        System.out.println("Items:    weight=" + java.util.Arrays.toString(weights));
        System.out.println("          value= " + java.util.Arrays.toString(values));
        System.out.println("Capacity: " + capacity);
        System.out.println();

        long max2D = solve2D(weights, values, capacity);
        System.out.println("2D DP max value: " + max2D);

        List<Integer> chosen = reconstruct(weights, values, capacity);
        System.out.print("Selected items (0-indexed): ");
        for (int idx : chosen) {
            System.out.printf("item[%d](w=%d, v=%d)  ", idx, weights[idx], values[idx]);
        }
        System.out.println();

        long max1D = solve1D(weights, values, capacity);
        System.out.println("1D DP max value: " + max1D);
        System.out.println();

        // Demonstrate the iteration direction bug
        System.out.println("=== Iteration Direction Bug Demo ===");
        int[]  singleItemWeights = {3};
        long[] singleItemValues  = {100L};
        int    bugCapacity       = 9;
        System.out.println("Single item: weight=3, value=100. Capacity=9.");
        System.out.println("Correct (0/1): should select item at most once. Max = 100.");
        System.out.println("Correct 1D result: " + solve1D(singleItemWeights, singleItemValues, bugCapacity));
        // Forward iteration would return 300 (item used 3 times) — do not ship that
    }
}
Output
=== 0/1 Knapsack Demo ===
Items: weight=[2, 2, 3]
value= [6, 10, 12]
Capacity: 5
2D DP max value: 22
Selected items (0-indexed): item[1](w=2, v=10) item[2](w=3, v=12)
1D DP max value: 22
=== Iteration Direction Bug Demo ===
Single item: weight=3, value=100. Capacity=9.
Correct (0/1): should select item at most once. Max = 100.
Correct 1D result: 100
The One Line That Determines Which Problem You Are Solving
for (int w = capacity; w >= weights[i]; w--) → 0/1 Knapsack (each item at most once) for (int w = weights[i]; w <= capacity; w++) → Unbounded Knapsack (each item unlimited times) These two loops look nearly identical. Both compile. Both run without errors. Both produce plausible-looking numbers. Only one of them solves the problem you intend to solve. If you take nothing else from this article, memorize this distinction.
Production Insight
Always use long for DP arrays and value inputs in production code. int silently overflows at 2,147,483,647 — roughly 2.1 billion. If you have 100 items each worth $30 million, your int-based DP will return garbage with no warning.
The 1D version cannot reconstruct which items were selected — the previous row is overwritten during each pass. If your use case requires knowing the selected items (and most production use cases do), the 2D version is not optional.
Add the weight-sum invariant assertion after reconstruction in production. It catches the left-to-right iteration bug, the array indexing bug, and future regression bugs in one check.
Rule: default to 2D for any new implementation. Switch to 1D only after profiling shows memory is the bottleneck, and only when the caller genuinely does not need item reconstruction.
Key Takeaway
The 2D table enables backtracking to find selected items — the backtracking traversal reads dp[i][w] against dp[i-1][w] to determine whether each item was taken.
Right-to-left iteration in the 1D version is not a style choice — it is the entire correctness guarantee for the 0/1 constraint.
Input validation and the weight-sum invariant assertion are not optional for production optimizers — silent wrong answers in resource allocation tools cause real financial damage.

Why the 1D Iteration Direction Is the Most Important Line in the Algorithm

The single most dangerous line in any 0/1 knapsack implementation is the inner loop of the 1D version. Not the recurrence, not the base case, not the array sizing — the loop direction. It has no compiler warning, no runtime exception, and no obvious symptom. It just silently computes the wrong answer.

To understand why, trace through what happens with one item (weight=3, value=100) and capacity=9.

Correct right-to-left pass: Start: dp = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0] w=9: dp[9] = max(dp[9], 100 + dp[9-3]) = max(0, 100 + dp[6]) = max(0, 100 + 0) = 100 w=8: dp[8] = max(0, 100 + dp[5]) = 100 w=7: dp[7] = max(0, 100 + dp[4]) = 100 w=6: dp[6] = max(0, 100 + dp[3]) = 100 w=5: dp[5] = max(0, 100 + dp[2]) = 100 w=4: dp[4] = max(0, 100 + dp[1]) = 100 w=3: dp[3] = max(0, 100 + dp[0]) = 100 Final dp[9] = 100. Item used once. Correct.

Broken left-to-right pass: Start: dp = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0] w=3: dp[3] = max(0, 100 + dp[0]) = 100 w=4: dp[4] = max(0, 100 + dp[1]) = 100 w=5: dp[5] = max(0, 100 + dp[2]) = 100 w=6: dp[6] = max(0, 100 + dp[3]) = 100 + 100 = 200 ← dp[3] already includes item w=7: dp[7] = max(0, 100 + dp[4]) = 200 w=8: dp[8] = max(0, 100 + dp[5]) = 200 w=9: dp[9] = max(0, 100 + dp[6]) = 100 + 200 = 300 ← item used three times Final dp[9] = 300. Item used three times. Wrong.

The left-to-right version returns 300 when the correct answer is 100. The item was used three times because each earlier update was already visible when a higher capacity position was evaluated. This is exactly unbounded knapsack — and it is exactly what happened in the production incident that over-provisioned VMs by 40%.

The One Line That Determines Which Problem You Are Solving
for (int w = capacity; w >= weights[i]; w--) → 0/1 Knapsack (each item at most once) for (int w = weights[i]; w <= capacity; w++) → Unbounded Knapsack (each item unlimited times) These two loops look nearly identical. Both compile. Both run without errors. Both produce plausible-looking numbers. Only one of them solves the problem you intend to solve. If you take nothing else from this article, memorize this distinction.
Production Insight
The left-to-right iteration bug is the most frequently shipped bug in knapsack-based production optimizers. It is invisible in code review unless the reviewer specifically knows to look for it.
Add a comment above every 1D knapsack inner loop in your codebase explicitly stating the iteration direction and why. Future engineers will not know to question it.
Rule: for any new knapsack implementation, write a unit test that places a single item whose weight divides evenly into capacity. The correct 0/1 answer selects that item once. If your test returns a multiple of the item value, the iteration direction is wrong.
Key Takeaway
Right-to-left iteration preserves the previous row's state at dp[w - weight[i]], enforcing the 0/1 constraint.
Left-to-right iteration uses the current row's already-updated state, silently solving unbounded knapsack instead.
This is not an implementation detail — it is the entire algorithmic difference between two fundamentally different problems.

Pseudo-Polynomial Time — Why O(n*W) Is Not As Fast As It Looks

The time complexity O(n × W) looks polynomial — two variables multiplied together. But it is actually pseudo-polynomial, and this distinction matters enormously when inputs scale.

Polynomial time means the runtime grows polynomially in the size of the input, where input size is the number of bits required to represent the input. For an algorithm that takes n items and a capacity W, the input size in bits is roughly n × log(max_weight) + n × log(max_value) + log(W). The key observation is that W itself is a value — a number — and the number of bits needed to represent W is log(W), not W.

When W = 10^9, the input needs about 30 bits to represent the capacity. But the DP table has 10^9 columns. That is exponential in the number of bits representing W, not polynomial. A truly polynomial algorithm would run in time polynomial in log(W) — roughly proportional to 30, not one billion.

The practical consequence: O(n × W) is fast when W is small. For n=100 items and W=10,000, the table has one million cells — runs in milliseconds. For n=100 items and W=10^9, the table has 100 billion cells — runs out of memory and time entirely. The algorithm that seemed fast completely collapses as W grows, not because of an implementation problem but because of a fundamental property of the algorithm.

This is why 0/1 knapsack is NP-complete. No polynomial-time algorithm is known (and finding one would prove P=NP). The DP approach is the best known exact algorithm, and it is viable only when W is bounded to a manageable value. When W is large, the practical alternatives are: approximation algorithms that trade exactness for speed (FPTAS can get within 1+ε of optimal in polynomial time), branch-and-bound with aggressive pruning, or problem reformulation that reduces the effective W.

Why W=10^9 Breaks the Algorithm Completely
  • W=1,000: table has 1,000 columns. Perfectly fine for any n.
  • W=100,000: table has 100,000 columns. Fine for most n values.
  • W=10,000,000: 2D table with n=100 rows uses roughly 8GB of memory for long values. Reaches memory limits.
  • W=10^9: completely infeasible with either 2D or 1D DP. No machine has 8 billion longs of RAM for a single solver.
  • W=10^18: the number of columns exceeds the addressable memory of any current hardware by orders of magnitude.
  • The size of W in bits is log2(W) — roughly 30 bits for W=10^9. An algorithm polynomial in the input size would run in time proportional to 30^k for some constant k, not 10^9.
Production Insight
Before implementing DP-based resource allocation in production, validate that W — the capacity dimension — is bounded to a value your servers can actually handle.
For cloud scheduling problems where W might represent available RAM in bytes, normalize W to a coarser granularity (megabytes, not bytes) to keep the DP table tractable.
Rule: if W exceeds 10^7 and you need an exact answer, DP is not the right algorithm. Reach for branch-and-bound, FPTAS, or a constraint programming solver.
Key Takeaway
O(n × W) is pseudo-polynomial — exponential in the number of bits representing W, not polynomial in input size.
0/1 Knapsack is NP-complete. The DP algorithm is not a polynomial-time solution — it is the best known exact algorithm for bounded W.
Always validate that W is tractable before deploying a DP-based optimizer. Normalize the capacity dimension if necessary.

The Two-Phase Attack: How to Actually Solve 0/1 Knapsack in an Interview

Most tutorials throw the DP table at you and call it a day. In the real world — and in every half-decent technical interview — you need two passes: a brute-force recursive solution to prove you understand the problem structure, then the optimized DP to show you can scale.

The recursive approach is your foundation. You start at item 0 with the full capacity. At each step you branch: include the current item if it fits, or skip it. The base case? No items left or no capacity. That's it. Two branches per call gives you O(2^n) time. Terrible for production, perfect for reasoning.

Once you've traced the recursion tree once, the DP table becomes obvious — you're just caching the overlapping subproblems. The recursive solution isn't dead code. It's your proof that the optimal substructure exists. Don't skip it in an interview unless you want the interviewer to suspect you memorized the table.

RecursiveKnapsack.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// io.thecodeforge — dsa tutorial

public class RecursiveKnapsack {
    private int[] itemWeights;
    private int[] itemValues;

    public RecursiveKnapsack(int[] weights, int[] values) {
        this.itemWeights = weights;
        this.itemValues = values;
    }

    public int solve(int remainingCapacity, int currentIndex) {
        if (currentIndex >= itemWeights.length || remainingCapacity <= 0) {
            return 0;
        }
        // Option 1: skip current item
        int valueIfSkipped = solve(remainingCapacity, currentIndex + 1);
        // Option 2: include current item if it fits
        int valueIfIncluded = 0;
        if (itemWeights[currentIndex] <= remainingCapacity) {
            valueIfIncluded = itemValues[currentIndex] 
                + solve(remainingCapacity - itemWeights[currentIndex], currentIndex + 1);
        }
        return Math.max(valueIfSkipped, valueIfIncluded);
    }

    public static void main(String[] args) {
        int[] weights = {1, 2, 4, 5};
        int[] values = {5, 4, 8, 6};
        RecursiveKnapsack knap = new RecursiveKnapsack(weights, values);
        System.out.println(knap.solve(5, 0));
    }
}
Output
13
Interview Pro Tip:
If you write the recursive solution first, then say 'now let's memoize it,' the interviewer knows you understand the genesis of DP. Jumping straight to the table looks like you memorized a pattern.
Key Takeaway
Always solve the recursion first. It's your proof of correctness, not your final answer.

Time Complexity Breakdown — Why O(n*W) Eats Your Lunch at Scale

Everyone parrots 'O(n*W) time, O(W) space' for the optimized 1D version. That's correct in theory, but in practice it's a trap. The W in the complexity is the capacity, which is an integer. If W is 10^9 and n is 100, you're looking at 100 billion operations. That's not 'pseudo-polynomial' — that's a coffee break turned into a full shift.

Here's the truth: the DP table's size depends on the magnitude of W, not its bit-length. That's why it's called pseudo-polynomial. If your capacity is 10^6, you're fine. If it's 10^9, your DP table needs gigabytes of memory and your loop runs for seconds. The greedy fractional knapsack runs in O(n log n). For huge capacities, that's your actual play.

Know your constraints. If the problem says 'capacity up to 10^5', DP is your friend. If it says 'capacity up to 10^9' and n < 30, use meet-in-the-middle. If n is large and capacity is huge, you need a different algorithm entirely. The DP isn't always the answer.

KnapsackComplexity.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// io.thecodeforge — dsa tutorial

public class KnapsackComplexity {
    // Optimized 1D DP — space efficient, same time
    public int solve(int[] weights, int[] values, int capacity) {
        int n = weights.length;
        int[] dp = new int[capacity + 1];
        
        // O(n*W) — this is the loop that kills you
        for (int item = 0; item < n; item++) {
            for (int w = capacity; w >= weights[item]; w--) {
                dp[w] = Math.max(dp[w], 
                    dp[w - weights[item]] + values[item]);
            }
        }
        return dp[capacity];
    }
    
    public static void main(String[] args) {
        // 100 items, capacity 10^9 — run this and watch your laptop burn
        KnapsackComplexity knap = new KnapsackComplexity();
        System.out.println("Never finishes with W=1e9");
    }
}
Output
Never finishes with W=1e9
Production Trap:
If you use a knapsack DP with a capacity of 10^9 in an API endpoint, you'll hit timeout on the first request. Always check constraints before reaching for the DP hammer.
Key Takeaway
O(n*W) is fast only when W is small. For large capacities, look for alternative algorithms.

The Hidden Cost — How Space Optimization Changes Your Reconstruction Strategy

You've seen the 1D optimization: roll the DP table from O(n*W) down to O(W). Clean, fast, interview-impressive. But here's the dirty secret — that single array wrecks your ability to reconstruct which items were picked. The optimized DP only tracks the best value for each capacity, not the path.

If your production system needs to actually produce the knapsack contents (not just the total value), you have two choices. First: keep the full 2D table and accept the memory hit. Second: store a separate boolean traceback array alongside your 1D DP, populating it only when you take an item. That second array is O(n*W) memory again — you just moved the cost, not eliminated it.

Senior engineers spot this trap immediately. The "optimized" version is worthless for warehouse picking, resource allocation, or any real inventory system. Always confirm whether the problem requires the item set or just the aggregate value before you gut the DP table.

KnapsackReconstruction.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// io.thecodeforge — dsa tutorial

public class KnapsackReconstruction {
    public static boolean[] reconstruct(int[] values, int[] weights, int capacity) {
        int n = values.length;
        int[][] dp = new int[n + 1][capacity + 1];
        for (int i = 1; i <= n; i++) {
            for (int w = 0; w <= capacity; w++) {
                if (weights[i - 1] <= w)
                    dp[i][w] = Math.max(dp[i - 1][w],
                        dp[i - 1][w - weights[i - 1]] + values[i - 1]);
                else dp[i][w] = dp[i - 1][w];
            }
        }
        boolean[] taken = new boolean[n];
        int w = capacity;
        for (int i = n; i > 0; i--) {
            if (dp[i][w] != dp[i - 1][w]) {
                taken[i - 1] = true;
                w -= weights[i - 1];
            }
        }
        return taken;
    }
}
Output
true (item 0, weight 2), false (item 1), true (item 2, weight 3) — when capacity = 5
Production Trap:
If you use the 1D DP and a client asks 'which items did I pack?', you're stuck with a ghost array or a full recomputation. Preserve the 2D table unless you've verified value-only output.
Key Takeaway
Space optimization kills traceback — always confirm output requirements before simplifying DP storage.

The Unbounded Trap — Why 0/1 Breaks When Items Are Reusable

Every junior dev I've interviewed has, at some point, treated the 0/1 Knapsack recurrence like a universal hammer. Then they hit a problem wording: 'unlimited copies of each item.' The code compiles, the tests pass for small cases, but the answer is wrong by a country mile. Why? Because the 0/1 recurrence forbids reuse by stepping back one row in the DP table — dp[i-1][w-wt]. That's the 'once per item' guarantee baked into the index.

Unbounded Knapsack flips this: you stay on the same row when you take an item, allowing infinite reuse. The DP becomes dp[i][w-wt] instead of dp[i-1][w-wt]. That single index change shifts the problem from combinatorial selection to unlimited repetition. Miss it, and you'll undercount value by assuming scarcity.

In production, this distinction kills resource provisioning systems. If you're building a packing optimizer for a factory that can order unlimited bolts, you use the unbounded variant. If orders are one-time limited runs, you use 0/1. The recurrence direction is your contract with the business logic — get it wrong and your warehouse ships half-empty crates.

UnboundedKnapsack.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
// io.thecodeforge — dsa tutorial

public class UnboundedKnapsack {
    public static int maxValue(int[] values, int[] weights, int capacity) {
        int[] dp = new int[capacity + 1];
        for (int i = 0; i < values.length; i++) {
            for (int w = weights[i]; w <= capacity; w++) {  // forward loop
                dp[w] = Math.max(dp[w], dp[w - weights[i]] + values[i]);
            }
        }
        return dp[capacity];
    }
}
Output
10 // items: value 5 weight 3, capacity 6 => take two copies, total value 10
Senior Shortcut:
Forward loop in the inner iteration = unbounded (reuse items). Backward loop = 0/1 (one per item). Memorize this. Your production logic depends on it.
Key Takeaway
0/1 Knapsack uses backward DP iteration to enforce single use — forward iteration unlocks the unbounded variant.
● Production incidentPOST-MORTEMseverity: high

Cloud Resource Allocator Over-Provisions by 40% — 1D Knapsack Iteration Bug

Symptom
Monthly cloud bills were consistently 40% higher than the optimizer's projected allocation. Individual workload assignments looked correct in isolation. Aggregate capacity was exceeded with no error messages, no exceptions, and no alerts. The system ran silently and returned results that appeared plausible — which is exactly why it took three months to catch.
Assumption
The engineering team spent the first two days assuming the input data contained duplicate VM entries or that the pricing API was returning stale capacity data. They added deduplication logic, re-ran the optimizer, and got the same wrong numbers. The input data was clean.
Root cause
The 1D space-optimized knapsack implementation iterated capacity left-to-right — from 0 up to W. This caused each VM instance to be selectable multiple times within a single optimization pass, because by the time the algorithm evaluated dp[w], the value at dp[w - weight[i]] had already been updated in the same iteration to include item i. The algorithm was silently solving unbounded knapsack instead of 0/1 knapsack. A single high-value VM with 8 vCPUs got assigned to three different workloads when only one physical instance existed.
Fix
1. Changed the capacity loop from for (w = 0; w <= W; w++) to for (w = W; w >= weights[i]; w--). This single character change — the direction of the inner loop — was the entire fix. 2. Added a post-optimization invariant assertion: the sum of selected item weights must not exceed capacity. This assertion would have caught the bug on day one. 3. Added a dedicated unit test verifying that a single high-value item is selected at most once regardless of how many times it could theoretically fit within capacity. 4. Deployed a capacity reconciliation job that runs nightly, comparing optimizer output against actual resource assignments and alerting on any over-allocation before billing closes.
Key lesson
  • Right-to-left iteration is 0/1 knapsack. Left-to-right iteration is unbounded knapsack. This is the single most critical implementation rule in the entire algorithm, and it produces no compiler warning and no runtime error when violated.
  • Silent wrong answers are worse than crashes. A crash stops the system. A silent wrong answer runs in production for months generating real financial damage. Always add invariant checks — total selected weight must not exceed capacity — to production optimizers.
  • The 1D space optimization trades debuggability for memory efficiency. When correctness is critical and the dataset is not memory-constrained, prefer the 2D table during initial deployment. Only optimize to 1D after the 2D version is validated and profiling proves the memory reduction is necessary.
  • Add a unit test that specifically checks the iteration direction: a single item that evenly divides capacity. The correct 0/1 answer uses it once; the wrong answer uses it many times. This test would have caught the incident in CI.
Production debug guideCommon symptoms when knapsack-based optimizers produce wrong results in production. Most of these have no stack trace — the algorithm runs cleanly and returns a wrong number.5 entries
Symptom · 01
Optimized allocation exceeds physical capacity by roughly one or more item's worth, with no error thrown
Fix
Check the 1D DP iteration direction immediately. Forward iteration (left-to-right) allows items to be reused within the same pass, silently converting 0/1 knapsack into unbounded knapsack. Change the inner loop to iterate from capacity down to weights[i]. Verify the fix by running a case where a single high-value item can fit multiple times — it should appear exactly once in the result.
Symptom · 02
Max value returned is negative or wildly incorrect despite valid inputs
Fix
Check whether int is used for the DP array or value inputs. Java silently wraps integer overflow with no exception — a value that should be 2,500,000,000 becomes a large negative number. Switch all DP arrays and value accumulators to long. Re-run with the same inputs and verify the sign.
Symptom · 03
ArrayIndexOutOfBoundsException thrown during DP table population
Fix
Verify DP array allocation dimensions. The table must be (n+1) by (W+1), not n by W. Row 0 represents the base case of zero items, and column 0 represents zero capacity — both must exist as dedicated rows and columns. Adding 1 to both dimensions is not optional.
Symptom · 04
Correct maximum value returned but reconstructed item list is wrong — either missing items or containing items that do not add up to the reported value
Fix
Item reconstruction requires the full 2D DP table. The 1D space-optimized version overwrites the previous row during each pass, making it impossible to backtrack through item decisions. If you need to know which items were selected — not just the maximum value — you must use the 2D implementation and run the backtracking traversal from dp[n][W].
Symptom · 05
Algorithm runs correctly on small test inputs but produces wrong results on production data with large W values
Fix
Check for integer overflow in the weight accumulator or in the dp[i-1][w - wt[i]] lookup when w - wt[i] produces a negative index due to a missed weight guard. Verify the weight guard condition wt[i] <= w is checked before computing w - wt[i]. Also verify that W itself has not been parsed from a string or configuration value that silently truncated a large number.
★ Knapsack Bug Quick DiagnosisSymptom-to-fix commands for production knapsack implementation failures.
Wrong max value that is suspiciously high — possibly 2x or 3x the expected result
Immediate action
Check the 1D iteration direction in the source file. This is the most common production bug and it has no runtime signal.
Commands
grep -n 'for.*w.*capacity\|for.*w.*W\|for.*w =\|w++\|w--' io/thecodeforge/dp/KnapsackSolver.java
grep -c 'w--\|w >=.*weights\|w >= wt' io/thecodeforge/dp/KnapsackSolver.java
Fix now
Change the inner loop from for (int w = 0; w <= capacity; w++) to for (int w = capacity; w >= weights[i]; w--). If the grep on the second command returns 0, the iteration direction is wrong.
Negative value or overflow in the returned maximum value+
Immediate action
Locate every DP array declaration and every value array declaration in the implementation file.
Commands
grep -n 'int\[\]\[\] dp\|int\[\] dp\|int\[\] values\|int\[\] val' io/thecodeforge/dp/KnapsackSolver.java
grep -n 'long\[\]\[\] dp\|long\[\] dp\|long\[\] values' io/thecodeforge/dp/KnapsackSolver.java
Fix now
Replace all int dp declarations with long. Replace int[] values with long[]. If the first grep returns results and the second returns nothing, overflow is your problem.
ArrayIndexOutOfBoundsException during DP table computation at runtime+
Immediate action
Locate the DP array allocation line and verify both dimensions include the +1 offset for base cases.
Commands
grep -n 'new.*\[.*\]\[.*\]\|new long\[\|new int\[' io/thecodeforge/dp/KnapsackSolver.java
grep -n 'n + 1\|n+1\|capacity + 1\|W + 1' io/thecodeforge/dp/KnapsackSolver.java
Fix now
Change dp[n][W] to dp[n+1][W+1] or dp[n+1][capacity+1]. If the second grep returns nothing, the base case rows and columns are missing.
2D vs 1D Knapsack Implementation — Complete Trade-off Analysis
Aspect2D DP Table — (n+1) × (W+1)1D Space-Optimized DP — (W+1)
Time ComplexityO(n × W) — n outer iterations, W+1 inner iterationsO(n × W) — identical runtime, different memory pattern
Space ComplexityO(n × W) — stores all n+1 rows simultaneouslyO(W) — stores only the current row, overwriting previous
Item ReconstructionYes — backtrack from dp[n][W] upward through the table to find which items were takenNo — the previous row is overwritten during each pass and cannot be recovered
Iteration DirectionInner loop can go left-to-right or right-to-left — row isolation prevents contaminationInner loop MUST go right-to-left — left-to-right silently solves unbounded knapsack
Silent Bug RiskLow — each row is computed from the previous row, which is never modified during the current row's computationHigh — the left-to-right bug produces wrong answers with no error signal and plausible-looking output
When to UseDefault choice. Required when the caller needs to know which items were selected, not just the maximum value.When only the maximum value is needed AND W is large enough that n × W memory is a real constraint.
Overflow RiskUse long throughout — int silently overflows when total value exceeds 2.1 billionSame — use long for all value arithmetic regardless of implementation variant
Large W FeasibilityInfeasible when W exceeds roughly 10^7 with n=100 rows due to memory — approximately 8GB for long valuesFeasible up to roughly W=10^9 with 8GB RAM for the single array, but runtime is still proportional to n × W
DebuggabilityHigh — print the full table to diagnose incorrect cells during developmentLow — the rolling array state at any given moment does not correspond to a human-interpretable snapshot

Key takeaways

1
0/1 Knapsack is NP-complete. The DP solution is pseudo-polynomial
fast only when capacity W is bounded.
2
Right-to-left iteration in the 1D version is not optional. Left-to-right silently solves unbounded knapsack with no error.
3
Always use long for DP arrays and value accumulators. int overflow causes silent negative results in production.
4
The 2D table enables item reconstruction via backtracking. Use it unless memory is truly constrained.
5
Add a unit test with a single item whose weight divides evenly into capacity
the correct 0/1 answer uses it once.
6
Input validation and the weight-sum invariant assertion catch the iteration direction bug and array indexing errors.
7
The recurrence is binary
skip (copy from above) or take (value + remainder from previous row).

Common mistakes to avoid

4 patterns
×

Sizing the DP array as dp[n][W] instead of dp[n+1][W+1]

Symptom
ArrayIndexOutOfBoundsException during table population, or — more dangerously — no exception but wrong answers because row 0 and column 0 base cases overwrite real item data when array bounds are wrong.
Fix
Always allocate dp[n+1][W+1]. Row 0 is the dedicated base case row representing zero items. Column 0 is the dedicated base case column representing zero capacity. Both must exist as standalone rows and columns that do not conflict with any item's data. Adding 1 to both dimensions is non-negotiable.
×

Iterating left-to-right in the 1D space-optimized version

Symptom
No exception, no crash, no warning — just wrong answers that are suspiciously high because items are counted multiple times within a single pass. The algorithm silently solves unbounded knapsack and returns results that look plausible until you audit the selected item counts.
Fix
Always iterate the capacity loop from capacity down to weights[i] in the 1D version: for (int w = capacity; w >= weights[i]; w--). Write this rule on a sticky note if you have to. Add a unit test that uses a single item whose weight divides evenly into capacity — the correct 0/1 answer selects that item exactly once, not capacity/weight times.
×

Using int instead of long for DP values or item values

Symptom
The returned maximum value is negative or wildly incorrect for large inputs. No overflow exception is thrown; Java silently wraps around.
Fix
Declare all DP arrays as long[][] or long[]. Store item values in long[] arrays. Use long for all intermediate sums. This single change prevents a whole class of silent overflow bugs that are nearly impossible to debug in production.
×

Off-by-one indexing when converting between 1-indexed DP rows and 0-indexed weight/value arrays

Symptom
The DP table produces correct results for the first few items but incorrect values for later items. The recurrence uses the wrong item weight or value, often causing the optimal value to be too low.
Fix
Access weights[i-1] and values[i-1] when iterating i from 1 to n. Always use the same conversion everywhere. Write a helper method getWeight(i) and getValue(i) that handles the offset so you never make this mistake again.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
What is the time complexity of the 0/1 knapsack DP solution and why is i...
Q02SENIOR
What is the difference between 0/1 knapsack and unbounded knapsack in te...
Q03SENIOR
How would you modify the 0/1 knapsack DP to also return the list of sele...
Q04SENIOR
Why does left-to-right iteration in the 1D version produce the wrong ans...
Q01 of 04SENIOR

What is the time complexity of the 0/1 knapsack DP solution and why is it called pseudo-polynomial?

ANSWER
The time complexity is O(n * W). It is pseudo-polynomial because it depends on the value of W, not the number of bits needed to represent W. The input size in bits includes log(W) bits for the capacity, but the algorithm runs in time proportional to W, which can be exponentially larger than log(W). For large W (e.g., 10^9), the DP is infeasible even though the input size is small.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
Can the 0/1 knapsack DP be used for fractional knapsack?
02
What is the maximum n and W for which the 2D DP table fits in memory?
03
How do I handle duplicate items or items with zero weight?
04
What is the difference between 0/1 knapsack and subset sum?
05
Can I use the 1D version if I need to know which items were selected?
N
Naren Founder & Principal Engineer

20+ years shipping performance-critical code where algorithms decide the bill. Drawn from code that ran under real load.

Follow
Verified
production tested
May 23, 2026
last updated
1,554
articles · all by Naren
🔥

That's Dynamic Programming. Mark it forged?

13 min read · try the examples if you haven't

Previous
Memoization vs Tabulation
3 / 15 · Dynamic Programming
Next
Longest Common Subsequence