1054-knapsack-01 — 0/1 Knapsack
Tutorial
The Problem
The 0/1 knapsack problem is the quintessential combinatorial optimization problem: given items with weights and values, and a weight capacity, select items to maximize total value without exceeding capacity. Each item can be included at most once (0 or 1 times). It has direct applications in resource allocation, financial portfolio optimization, and load balancing.
The DP solution runs in O(n × capacity) time — pseudo-polynomial because it depends on the numeric value of capacity, not just the number of items.
🎯 Learning Outcomes
Code Example
#![allow(clippy::all)]
// 1054: 0/1 Knapsack — 2D DP Table
use std::collections::HashMap;
// Approach 1: 2D Vec DP
fn knapsack_2d(weights: &[usize], values: &[i64], capacity: usize) -> i64 {
let n = weights.len();
let mut dp = vec![vec![0i64; capacity + 1]; n + 1];
for i in 1..=n {
for w in 0..=capacity {
dp[i][w] = dp[i - 1][w];
if weights[i - 1] <= w {
dp[i][w] = dp[i][w].max(dp[i - 1][w - weights[i - 1]] + values[i - 1]);
}
}
}
dp[n][capacity]
}
// Approach 2: 1D rolling array
fn knapsack_1d(weights: &[usize], values: &[i64], capacity: usize) -> i64 {
let mut dp = vec![0i64; capacity + 1];
for i in 0..weights.len() {
for w in (weights[i]..=capacity).rev() {
dp[w] = dp[w].max(dp[w - weights[i]] + values[i]);
}
}
dp[capacity]
}
// Approach 3: Recursive with HashMap memoization
fn knapsack_memo(weights: &[usize], values: &[i64], capacity: usize) -> i64 {
fn solve(
i: usize,
w: usize,
weights: &[usize],
values: &[i64],
cache: &mut HashMap<(usize, usize), i64>,
) -> i64 {
if i == 0 || w == 0 {
return 0;
}
if let Some(&v) = cache.get(&(i, w)) {
return v;
}
let skip = solve(i - 1, w, weights, values, cache);
let take = if weights[i - 1] <= w {
solve(i - 1, w - weights[i - 1], weights, values, cache) + values[i - 1]
} else {
0
};
let result = skip.max(take);
cache.insert((i, w), result);
result
}
let mut cache = HashMap::new();
solve(weights.len(), capacity, weights, values, &mut cache)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_knapsack_2d() {
assert_eq!(knapsack_2d(&[2, 3, 4, 5], &[3, 4, 5, 6], 5), 7);
assert_eq!(knapsack_2d(&[1, 2, 3], &[6, 10, 12], 5), 22);
}
#[test]
fn test_knapsack_1d() {
assert_eq!(knapsack_1d(&[2, 3, 4, 5], &[3, 4, 5, 6], 5), 7);
assert_eq!(knapsack_1d(&[1, 2, 3], &[6, 10, 12], 5), 22);
}
#[test]
fn test_knapsack_memo() {
assert_eq!(knapsack_memo(&[2, 3, 4, 5], &[3, 4, 5, 6], 5), 7);
assert_eq!(knapsack_memo(&[1, 2, 3], &[6, 10, 12], 5), 22);
}
}Key Differences
downto syntax**: OCaml's for w = capacity downto weights.(i) expresses reverse iteration clearly; Rust uses .rev() on a range.weights.(i) is direct array access; Rust's weights[i] is equivalent with bounds checking.dp[n][capacity] backward.OCaml Approach
let knapsack weights values capacity =
let n = Array.length weights in
let dp = Array.make (capacity + 1) 0 in
for i = 0 to n - 1 do
for w = capacity downto weights.(i) do
dp.(w) <- max dp.(w) (dp.(w - weights.(i)) + values.(i))
done
done;
dp.(capacity)
The downto keyword makes the reverse iteration clear in OCaml. The structure is identical to Rust's approach.
Full Source
#![allow(clippy::all)]
// 1054: 0/1 Knapsack — 2D DP Table
use std::collections::HashMap;
// Approach 1: 2D Vec DP
fn knapsack_2d(weights: &[usize], values: &[i64], capacity: usize) -> i64 {
let n = weights.len();
let mut dp = vec![vec![0i64; capacity + 1]; n + 1];
for i in 1..=n {
for w in 0..=capacity {
dp[i][w] = dp[i - 1][w];
if weights[i - 1] <= w {
dp[i][w] = dp[i][w].max(dp[i - 1][w - weights[i - 1]] + values[i - 1]);
}
}
}
dp[n][capacity]
}
// Approach 2: 1D rolling array
fn knapsack_1d(weights: &[usize], values: &[i64], capacity: usize) -> i64 {
let mut dp = vec![0i64; capacity + 1];
for i in 0..weights.len() {
for w in (weights[i]..=capacity).rev() {
dp[w] = dp[w].max(dp[w - weights[i]] + values[i]);
}
}
dp[capacity]
}
// Approach 3: Recursive with HashMap memoization
fn knapsack_memo(weights: &[usize], values: &[i64], capacity: usize) -> i64 {
fn solve(
i: usize,
w: usize,
weights: &[usize],
values: &[i64],
cache: &mut HashMap<(usize, usize), i64>,
) -> i64 {
if i == 0 || w == 0 {
return 0;
}
if let Some(&v) = cache.get(&(i, w)) {
return v;
}
let skip = solve(i - 1, w, weights, values, cache);
let take = if weights[i - 1] <= w {
solve(i - 1, w - weights[i - 1], weights, values, cache) + values[i - 1]
} else {
0
};
let result = skip.max(take);
cache.insert((i, w), result);
result
}
let mut cache = HashMap::new();
solve(weights.len(), capacity, weights, values, &mut cache)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_knapsack_2d() {
assert_eq!(knapsack_2d(&[2, 3, 4, 5], &[3, 4, 5, 6], 5), 7);
assert_eq!(knapsack_2d(&[1, 2, 3], &[6, 10, 12], 5), 22);
}
#[test]
fn test_knapsack_1d() {
assert_eq!(knapsack_1d(&[2, 3, 4, 5], &[3, 4, 5, 6], 5), 7);
assert_eq!(knapsack_1d(&[1, 2, 3], &[6, 10, 12], 5), 22);
}
#[test]
fn test_knapsack_memo() {
assert_eq!(knapsack_memo(&[2, 3, 4, 5], &[3, 4, 5, 6], 5), 7);
assert_eq!(knapsack_memo(&[1, 2, 3], &[6, 10, 12], 5), 22);
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_knapsack_2d() {
assert_eq!(knapsack_2d(&[2, 3, 4, 5], &[3, 4, 5, 6], 5), 7);
assert_eq!(knapsack_2d(&[1, 2, 3], &[6, 10, 12], 5), 22);
}
#[test]
fn test_knapsack_1d() {
assert_eq!(knapsack_1d(&[2, 3, 4, 5], &[3, 4, 5, 6], 5), 7);
assert_eq!(knapsack_1d(&[1, 2, 3], &[6, 10, 12], 5), 22);
}
#[test]
fn test_knapsack_memo() {
assert_eq!(knapsack_memo(&[2, 3, 4, 5], &[3, 4, 5, 6], 5), 7);
assert_eq!(knapsack_memo(&[1, 2, 3], &[6, 10, 12], 5), 22);
}
}
Deep Comparison
0/1 Knapsack — Comparison
Core Insight
The 0/1 knapsack builds a 2D table where dp[i][w] = max value using first i items with capacity w. The 1D optimization exploits the fact that each row only depends on the previous row, with reverse iteration ensuring items aren't reused.
OCaml Approach
Array.init for 2D arrays — creates array of arraysfor w = capacity downto weight for reverse iteration in 1D versionHashtbl with tuple keys (i, w) for memoizationfind_opt for cache accessRust Approach
vec![vec![0; cap+1]; n+1] for 2D table(weight..=cap).rev() for reverse range — idiomatic RustHashMap<(usize, usize), i64> for memoization with tuple keys&mut)Comparison Table
| Aspect | OCaml | Rust |
|---|---|---|
| 2D table | Array.init (n+1) (fun _ -> Array.make ...) | vec![vec![0; cap+1]; n+1] |
| Reverse iteration | for w = cap downto wt | (wt..=cap).rev() |
| Tuple hash key | (i, w) — works directly | (usize, usize) — Hash auto-derived |
| Space optimization | 1D array with downto | 1D vec with .rev() range |
| Max function | max (built-in) | .max() method on i64 |
Exercises
Vec<usize> of selected item indices alongside the maximum value.