Computer Science / Dynamic Programming & 0/1 Knapsack
FREE TO EXPLORE
LABORATORY 11 · COMPUTER SCIENCE

Dynamic Programming & 0/1 Knapsack theory

Understand dynamic programming & 0/1 knapsack: recurrence relations, Big-O bounds, decision trees and hardware cache interactions.

Theoretical Foundations of Dynamic Programming & 0/1 Knapsack and Computational Complexity

Dynamic Programming (DP), formulated by Richard Bellman in the 1950s, is one of the most powerful algorithmic paradigms in modern computing, providing polynomial-time solutions to complex multi-stage decision and optimization problems. While Divide-and-Conquer algorithms partition problems into independent disjoint subproblems (as in Merge Sort or Quick Sort), Dynamic Programming addresses problems characterized by two fundamental properties: (1) Optimal Substructure, where the globally optimal solution can be constructed from optimal solutions to its constituent subproblems; and (2) Overlapping Subproblems, where a naive recursive formulation repeatedly evaluates the identical subproblem states an exponential number of times. The canonical formulation of Dynamic Programming is exemplified by the 0/1 Knapsack Problem: given n discrete items each with an integer weight w_i and valuation v_i, select a binary inclusion subset x_i ∈ {0, 1} to maximize total valuation ∑(v_i x_i) subject to a total weight capacity constraint ∑(w_i x_i) ≤ W. The Bellman optimality recurrence states that for item i and remaining weight capacity w, the maximum achievable value is DP[i][w] = max(DP[i-1][w], DP[i-1][w - w_i] + v_i) when w ≥ w_i, and DP[i-1][w] otherwise. In a naive recursive tree, this recurrence branches into 2^n paths, taking Θ(2^n) exponential time. Dynamic Programming eliminates redundant recalculation via two distinct strategies: (a) Top-Down Memoization, which retains recursive decomposition while caching computed subproblem evaluations in an auxiliary hash table or lookup array on first visit; and (b) Bottom-Up Tabulation, which systematically populates the (n + 1) × (W + 1) state matrix in topological row-major order. In Bottom-Up Tabulation, each state cell is evaluated in strict O(1) arithmetic operations, achieving a deterministic running time of T(n, W) = n · W operations. Because this running time is polynomial in the integer magnitude of W but exponential in the binary bit-length log₂ W of the capacity, 0/1 Knapsack is classified as Weakly NP-Complete, admitting pseudo-polynomial time dynamic programming. Benchmarking subproblem evaluations across systematically scaled item counts verifies the exact linear scaling law T(n) = 100.0000 · n, while elucidating critical memory hierarchy phenomena including L1/L2 cache locality, spatial prefetching, and memory reduction from Θ(n W) to Θ(W) via 1D reverse-scanned rolling buffers.

TOPIC 01 · FORMAL ANALYSIS

What is the formal definition of the Dynamic Programming paradigm, and how do Optimal Substructure and Overlapping Subproblems distinguish it from Divide-and-Conquer?

Dynamic Programming (DP), formalized by Richard Bellman in 1957, is a foundational algorithmic optimization paradigm designed to solve multi-stage decision problems by decomposing them into smaller subproblems, solving each subproblem exactly once, and storing their solutions to avoid redundant recalculation. The applicability of Dynamic Programming is fundamentally governed by two formal mathematical conditions:



1. Optimal Substructure: A problem exhibits optimal substructure if an optimal solution to the overall problem contains within it optimal solutions to its constituent subproblems. Mathematically, this is expressed through Bellman's Principle of Optimality: 'An optimal policy has the property that whatever the initial state and initial decision are, the remaining decisions must constitute an optimal policy with regard to the state resulting from the first decision.' In the 0/1 Knapsack Problem, if an optimal selection of items for capacity W includes item i with weight w_i and value v_i, then the remaining selected items must constitute an optimal knapsack packing for the reduced capacity W - w_i among the remaining n - 1 items. If a superior subproblem combination existed, it could be substituted to yield a higher overall value, contradicting the assumed optimality of the original solution (a cut-and-paste argument).



2. Overlapping Subproblems: A problem exhibits overlapping subproblems if a naive recursive formulation visits the same subproblems repeatedly across different branches of the decision tree. This property fundamentally distinguishes Dynamic Programming from Divide-and-Conquer. In Divide-and-Conquer algorithms (such as Merge Sort or Quick Sort), a problem is partitioned into completely disjoint (independent) subproblems: sorting the left half of an array is entirely independent of sorting the right half, and subproblems never share common state. Consequently, Divide-and-Conquer evaluates each subproblem without redundant repetition.



In contrast, optimization problems like Knapsack, Longest Common Subsequence (LCS), and Shortest Paths exhibit densely overlapping subproblems. For example, in computing the n-th Fibonacci number naively via F(n) = F(n-1) + F(n-2), the subproblem F(n-2) is computed independently by the left branch and again by the right branch; F(n-3) is evaluated 3 times; and in general, the recursion tree expands to 2^n calls, performing an exponential number of duplicate operations. Dynamic Programming resolves this inefficiency by transforming the exponential recursion tree into a polynomial Directed Acyclic Graph (DAG) of subproblem dependencies, solving each distinct node in the DAG exactly once.

TOPIC 02 · FORMAL ANALYSIS

How do Top-Down Memoization and Bottom-Up Tabulation differ in execution flow, memory allocation, cache locality, and recursion stack overhead?

Dynamic Programming can be implemented using two distinct execution strategies: Top-Down Memoization and Bottom-Up Tabulation. While both achieve identical asymptotic time complexity O(n·W), their concrete runtime performance and systems characteristics differ substantially:



1. Top-Down Memoization (On-Demand Recursive Evaluation):

In top-down memoization, the problem retains its natural recursive mathematical definition. A global or heap-allocated lookup table (such as a 2D array or hash map) is initialized with a sentinel value (typically -1) indicating that a subproblem has not yet been solved. When function DP(i, w) is called:

- It first checks if table[i][w] ≠ -1. If so (a cache hit), it returns the stored result immediately in O(1) time.

- If table[i][w] == -1 (a cache miss), it recursively evaluates the necessary subproblem branches, computes the optimal result, writes it to table[i][w], and returns it.

Advantages: Memoization evaluates only reachable subproblems. If large portions of the state space (n × W) are never visited along any valid decision path, memoization skips them entirely, potentially running faster than full table scans.

Disadvantages: (a) Call stack overhead: Each recursive call allocates a stack frame on the thread execution stack (AMD64 System V ABI allocates 48–64 bytes per frame). For deep recursions (n = 100,000), this risks call stack exhaustion (Stack Overflow); (b) Poor CPU cache locality: Recursive function calls jump unpredictably across memory addresses, causing cold L1/L2 instruction cache and data cache misses; (c) Function call overhead: Register spills, return address pushes, and stack pointer updates introduce non-trivial microarchitectural latency.



2. Bottom-Up Tabulation (Iterative Topological Evaluation):

In bottom-up tabulation, the subproblem dependency graph (DAG) is linearized into a topological order. An iterative loop evaluates subproblems starting from the smallest base cases (e.g., i = 0 or w = 0) and progressively builds up to the target state DP[n][W].

Advantages: (a) Zero recursion overhead: Tabulation executes via plain iterative loops (for i = 1 to n; for w = 0 to W), eliminating stack frame allocation entirely; (b) Optimal CPU cache locality: By allocating the table as a contiguous 2D array and traversing it in row-major order (outer loop i, inner loop w), the CPU accesses contiguous memory addresses. This maximizes L1/L2 data cache line utilization (64 bytes per line = 8 integers fetched simultaneously) and activates hardware stream prefetchers; (c) Deterministic execution: Iteration proceeds with minimal branch mispredictions.

Disadvantages: Tabulation computes every single state in the (n + 1) × (W + 1) grid, even if some intermediate states do not contribute to the final answer DP[n][W].

TOPIC 03 · FORMAL ANALYSIS

What is the mathematical formulation of the 0/1 Knapsack Problem, and how is the Bellman recurrence relation derived and solved?

The 0/1 Knapsack Problem is a canonical NP-complete combinatorial optimization problem with pervasive applications in resource allocation, cryptography, network packet scheduling, and financial portfolio management.



1. Formal Problem Statement:

Given a set of n items indexed from 1 to n, where each item i possesses:

- An integer weight w_i > 0

- An integer valuation v_i > 0

and a knapsack with maximum weight capacity W > 0. The goal is to select a subset of items to maximize total valuation without exceeding capacity:

Maximize: ∑_{i=1}^n v_i x_i

Subject to: ∑_{i=1}^n w_i x_i ≤ W, where x_i ∈ {0, 1} for each i ∈ {1, 2, ..., n}

The binary decision variable x_i represents whether item i is excluded (x_i = 0) or included (x_i = 1) in the knapsack. The '0/1' constraint strictly forbids fractional item selection (which would correspond to the Fractional Knapsack problem solvable via a greedy algorithm in O(n log n) time).



2. Derivation of the Bellman Recurrence:

Let DP[i][w] denote the maximum value achievable considering only the first i items (subset {1, 2, ..., i}) with a weight budget of w (where 0 ≤ w ≤ W).

When deciding on item i, there are exactly two mutually exclusive possibilities:

- Case 1: Exclude item i (x_i = 0). The maximum value achievable is simply the best value obtained from the first i - 1 items using the full weight budget w: Value = DP[i - 1][w].

- Case 2: Include item i (x_i = 1). This choice is only feasible if the remaining capacity w is at least the item weight: w ≥ w_i. In this case, item i contributes its valuation v_i, and the remaining budget becomes w - w_i. The optimal value achievable for the remaining capacity from the first i - 1 items is DP[i - 1][w - w_i]. Total Value = DP[i - 1][w - w_i] + v_i.



By Bellman's Principle of Optimality, the optimal decision is the maximum of these two alternatives:

DP[i][w] = DP[i - 1][w] if w < w_i

DP[i][w] = max(DP[i - 1][w], DP[i - 1][w - w_i] + v_i) if w ≥ w_i



3. Base Cases:

- DP[0][w] = 0 for all 0 ≤ w ≤ W (with 0 items available, the achievable value is 0).

- DP[i][0] = 0 for all 0 ≤ i ≤ n (with 0 weight capacity, no positive-weight item can be accommodated).



The final solution to the global problem resides in DP[n][W]. Populating this table requires filling (n + 1) × (W + 1) cells. Because each cell evaluates at most one comparison and one addition in O(1) time, total running time is exactly Θ(n·W).

TOPIC 04 · FORMAL ANALYSIS

Why is the 0/1 Knapsack Problem classified as Weakly NP-Complete, and what is the precise distinction between Pseudo-Polynomial and Polynomial time complexity?

In computational complexity theory, the time complexity of an algorithm is formally measured as a function of the input length in bits, denoted by L. Understanding this formal definition is essential to resolving the apparent paradox: why is 0/1 Knapsack an NP-complete problem, yet solvable in 'O(n·W)' time via Dynamic Programming?



1. Input Bit-Length vs. Numeric Magnitude:

An instance of the 0/1 Knapsack problem consists of n items with weights w_1, ..., w_n, valuations v_1, ..., v_n, and capacity W. How many bits are required to encode this input? Each number is written in binary notation. Storing capacity W requires ⌈log₂ W⌉ bits. Storing weights and values requires ∑ (⌈log₂ w_i⌉ + ⌈log₂ v_i⌉) bits. The total input length L is approximately:

L = O(n · log₂ W + n · log₂ (max v_i))

Notice that the input length scales logarithmically with W: L_W = log₂ W, which means W = 2^{L_W}.



2. Pseudo-Polynomial Time Complexity:

An algorithm is defined to run in Polynomial Time if its running time is bounded by a polynomial in the input bit-length L, i.e., T(L) = O(L^k) for some constant k. The dynamic programming algorithm for 0/1 Knapsack runs in T = O(n·W) operations. Substituting W = 2^{L_W}:

T = O(n · 2^{L_W})

The running time is exponential in the number of bits required to encode W! If an adversary provides a capacity W = 2⁶⁴ (an integer that fits in a single 64-bit machine register, so L_W = 64 bits), the dynamic programming algorithm requires 2⁶⁴ ≈ 1.84 × 10¹⁹ operations—an astronomical runtime that would take centuries to execute.



An algorithm whose running time is polynomial in the numeric magnitude of the input values (W), but exponential in their bit-length (log₂ W), is termed a Pseudo-Polynomial Time Algorithm.



3. Weak vs. Strong NP-Completeness:

- Weakly NP-Complete Problems: Problems (like 0/1 Knapsack and Subset Sum) that are NP-complete in general, but admit pseudo-polynomial time algorithms when numerical inputs are bounded. If the numeric values of W are bounded by a polynomial in n (e.g., W ≤ n²), then O(n·W) = O(n³), which is genuinely polynomial time! Furthermore, weakly NP-complete problems admit Fully Polynomial-Time Approximation Schemes (FPTAS), computing a (1 - ε)-optimal solution in O(n³/ε) time.

- Strongly NP-Complete Problems: Problems (like Traveling Salesperson, 3-SAT, or Vertex Cover) that remain NP-complete even when all numerical values are polynomially bounded in n (or contain no numerical values at all). Strongly NP-complete problems cannot be solved in pseudo-polynomial time unless P = NP.

TOPIC 05 · FORMAL ANALYSIS

How can the spatial memory complexity of 0/1 Knapsack Tabulation be compressed from Θ(n·W) down to Θ(W) using rolling arrays and 1D reverse scanning?

While the standard 2D tabulation matrix for 0/1 Knapsack requires Θ(n·W) memory space, careful inspection of the Bellman recurrence reveals that full 2D history is entirely redundant if only the maximum value DP[n][W] is required.



1. The Temporal Locality Observation:

Examine the recurrence: DP[i][w] = max(DP[i - 1][w], DP[i - 1][w - w_i] + v_i). Notice that computing the entries of row i depends strictly and exclusively on the entries of row i - 1! Row i never references row i - 2, row i - 3, or any earlier history. Once row i has been fully computed, row i - 1 is never accessed again.



2. Two-Row Rolling Buffer (Space: 2 × (W + 1)):

The most immediate optimization is a rolling buffer: allocate an array of dimensions 2 × (W + 1). In step i, row i % 2 is updated using entries from row (i - 1) % 2. This immediately reduces memory consumption from Θ(n·W) down to Θ(W) while preserving the exact forward loop structure.



3. 1D Array with Reverse Scanning (Space: W + 1):

Can we reduce memory even further, utilizing a single 1D array of length W + 1? Yes, by leveraging directionality in the capacity loop!

Suppose we maintain a single 1D array `dp[w]` initialized to 0. If we update `dp[w]` for item i by iterating w forward from w_i to W:

`for (int w = w_i; w <= W; w++) dp[w] = max(dp[w], dp[w - w_i] + v_i);`

This produces a catastrophic bug for 0/1 Knapsack! Because w increases, `dp[w - w_i]` has already been updated in the current loop iteration for item i. Consequently, item i can be included multiple times (at w_i, 2w_i, 3w_i...), inadvertently solving the Unbounded Knapsack Problem (where items have infinite supply)!



To preserve the 0/1 constraint (each item included at most once), we must iterate the capacity w backwards from W down to w_i:

`for (int w = W; w >= w_i; w--) dp[w] = max(dp[w], dp[w - w_i] + v_i);`

When computing `dp[w]` with backwards iteration, the value `dp[w - w_i]` on the right-hand side has not yet been updated in the current iteration; it still contains the result from step i - 1! Thus, each item i is considered at most once.



4. Spatial Impact:

For n = 1,000 items and capacity W = 100,000, a standard 2D table of 64-bit integers requires (1000 + 1) × (100000 + 1) × 8 bytes ≈ 800 MB of RAM. The 1D reverse-scanned array requires only (100000 + 1) × 8 bytes ≈ 800 KB of RAM—a 1000-fold memory reduction that fits entirely inside the CPU L3/L2 cache, dramatically speeding up wall-clock execution.

TOPIC 06 · FORMAL ANALYSIS

What is the Backtracking Reconstruction algorithm to recover the exact subset of selected items from a 2D Tabulation Table, and what are its complexity bounds?

While the cell DP[n][W] contains the numeric maximum valuation achievable, real-world systems require knowing the exact subset of items that produce this optimal value. Recovering this binary decision vector x ∈ {0, 1}^n is accomplished through Backtracking Reconstruction.



1. Reconstruction Algorithm Mechanics:

Starting at the terminal cell (i = n, w = W), the algorithm traces backward through the decision matrix to determine whether item i was included or excluded:

- Comparison Test: Compare DP[i][w] with DP[i - 1][w].

- Case A (Item i Excluded): If DP[i][w] == DP[i - 1][w], then the optimal value at capacity w could be achieved without including item i. We set x_i = 0 and transition to state (i - 1, w).

- Case B (Item i Included): If DP[i][w] ≠ DP[i - 1][w], then item i must have contributed to the optimal solution. (Note: even if DP[i][w] == DP[i - 1][w - w_i] + v_i, we only include item i if its inclusion yields a strictly superior or chosen alternative). We set x_i = 1, decrement the remaining capacity budget w ← w - w_i, and transition to state (i - 1, w - w_i).



This process repeats decrementing i from n down to 1. The loop terminates when i reaches 0 or remaining capacity w reaches 0.



2. Complexity Bounds:

- Time Complexity: At each step, i decrements by exactly 1. Therefore, the backtracking path visits at most n cells. At each cell, it performs a single equality comparison and optional subtraction in O(1) time. The overall time complexity of backtracking reconstruction is strictly O(n) linear time, which is completely negligible compared to the O(n·W) table generation time.

- Space Complexity: Backtracking requires inspecting the 2D table DP[i][w]. Therefore, if the exact item subset must be reconstructed, the full 2D matrix (or an equivalent checkpointing / bit-vector log) must be preserved in memory, requiring Θ(n·W) space.



3. Space-Efficient Item Recovery (Hirschberg's Divide-and-Conquer Technique):

If Θ(n·W) memory cannot be accommodated on memory-constrained systems, can we reconstruct the optimal subset using only Θ(W) space? Yes, using Hirschberg's algorithm (a divide-and-conquer adaptation of DP). By running forward DP on the first n/2 items and backward DP on the remaining n/2 items, the optimal split point w* at row n/2 can be located in O(W) space and O(n·W) time. Recursing on both halves yields the complete item subset in O(n·W) time and strictly O(W) space.

TOPIC 07 · FORMAL ANALYSIS

How do memory layout and CPU cache architecture (L1/L2 data cache lines, row-major vs. column-major ordering) affect the wall-clock performance of 2D Tabulation loops?

In modern computer systems, theoretical operation counts (Big-O complexity) do not tell the complete story of execution latency. Hardware memory hierarchy dynamics—specifically CPU cache lines, memory strides, and spatial/temporal locality—profoundly dictate the wall-clock throughput of Dynamic Programming tabulation loops.



1. Memory Layout and Cache Lines:

In modern microprocessors (x86-64 and ARM64), system memory is organized in a hierarchy: L1 Cache (~32–64 KB, ~1–4 cycles latency), L2 Cache (~512 KB–1 MB, ~10–14 cycles), L3 Cache (~16–64 MB, ~30–50 cycles), and main DRAM (~16–64 GB, ~150–250 cycles). When a CPU reads a single 64-bit integer from memory, it does not fetch 8 bytes in isolation; instead, the memory controller fetches an entire Cache Line, which is standardized at 64 bytes (8 contiguous 64-bit integers).



2. Row-Major Traversal (Outer Loop i, Inner Loop w):

In languages like C, C++, Python, and Rust, 2D arrays are stored in Row-Major format: all elements of row 0 reside consecutively in memory, followed immediately by row 1, then row 2, and so on. In row-major traversal:

```c

for (int i = 1; i <= n; i++) {

for (int w = 0; w <= W; w++) {

// DP[i][w] accessed consecutively: address = base + (i * (W + 1) + w) * 8

}

}

```

In this inner loop, each increment of w accesses the immediately adjacent 8-byte memory address. When w = 0 is accessed, the CPU incurs an L1 cache miss, but the hardware loads 64 bytes into the cache line, bringing DP[i][0] through DP[i][7] into L1 cache simultaneously! The next 7 iterations (w = 1 to 7) execute with instantaneous L1 cache hits (~1 cycle). Furthermore, hardware Stream Prefetchers detect this linear sequential memory stride and proactively stream subsequent cache lines from DRAM before the CPU even requests them, achieving near 100% cache utilization and maximum memory bus bandwidth.



3. Column-Major Traversal (Outer Loop w, Inner Loop i):

Suppose a developer inadvertently swaps the loop nesting order:

```c

for (int w = 0; w <= W; w++) {

for (int i = 1; i <= n; i++) {

// DP[i][w] accessed with stride (W + 1) * 8 bytes!

}

}

```

In this transposed loop, consecutive iterations access DP[1][w], DP[2][w], DP[3][w]... Each successive access jumps by (W + 1) × 8 bytes. If W = 10,000, each jump skips 80,000 bytes—vastly exceeding the 64-byte cache line size! Every single memory access misses the L1 cache, misses the L2 cache, flushes the Translation Lookaside Buffer (TLB), and stalls the CPU execution pipeline waiting for DRAM (~200 clock cycles per cell).



Empirical benchmarks demonstrate that transposed column-major traversal runs 15× to 40× slower than row-major traversal on identical hardware, despite executing the exact same mathematical number of operations n·W. Mastering memory hierarchy alignment is therefore paramount for high-performance algorithm implementation.

↳
Connect the theory to benchmark observations.

In Bottom-Up Tabulation of the 0/1 Knapsack problem with fixed capacity W = 100 weight units, every subproblem cell DP[i][w] across the (n + 1) × (W + 1) state matrix is computed exactly once in O(1) time using the Bellman recurrence relation. Across n items, the total number of elementary state transition evaluations scales as T(n) = W · n = 100.0000 · n operations with exact theoretical intercept T_0 = 0.0000 states.