DSA DSA In Depth
0%
lesson knapsackdynamic-programmingmonotonic-queue

The Knapsack Family: 0/1, Unbounded, Subset Sum Bitset & Bounded Knapsack (Binary Splitting / Monotonic Queue)

7 min read Module 6.2: Dynamic Programming Foundations & Classical Archetypes Updated Oct 23, 2026
📄 Cheat Sheet
0/1 Knapsack, Unbounded Knapsack & Subset Sum Dynamic Programming
Tushar Roy - Coding Made Simple 0/1 Knapsack, Unbounded Knapsack & Subset Sum Dynamic Programming Watch on YouTube ↗

Clear walkthrough of the 0/1 vs Unbounded state transitions and 1D space-compressed loop directions.

0:00 / --:--

1. The Four Canonical Knapsack Variants

Given $N$ item types with weights $w_i$, values $v_i$, and knapsack capacity $W$:

  1. 0/1 Knapsack ($c_i \in \{0, 1\}$): Each item taken at most once. 1D recurrence iterates $w$ backward from $W$ down to $w_i$: $$dp[w] = \max(dp[w],\; dp[w - w_i] + v_i)$$
  2. Unbounded / Complete Knapsack ($c_i \in \{0, 1, 2, \dots\}$): Unlimited copies of each item. 1D recurrence iterates $w$ forward from $w_i$ up to $W$ so $dp[w - w_i]$ already includes item $i$: $$dp[w] = \max(dp[w],\; dp[w - w_i] + v_i)$$
  3. Subset Sum Bitset Acceleration ($v_i = 0/1$ feasibility): Can a subset sum to $w$? Pack the boolean array into 64-bit machine words (std::bitset). Adding item $w_i$ becomes a single word-parallel shift-OR: $$\text{bits} \mathrel{|=} (\text{bits} \ll w_i) \quad \implies \quad O\!\left(\frac{N \cdot W}{64}\right)$$
  4. Bounded Knapsack ($c_i \in \{0, 1, \dots, k_i\}$): At most $k_i$ copies of item $i$. Naive transition checks all $c \in [0..k_i]$ in $O(N W \cdot k_i)$. How do we remove the $k_i$ factor?

Core Invariant — Binary Splitting ($O(N W \log K)$) & Monotonic Queue ($O(N W)$) for Bounded Knapsack

Method 1 (Binary Splitting): Any integer $k_i$ can be decomposed into $\lfloor \log_2 k_i \rfloor + 1$ bundles with sizes $1, 2, 4, \dots, 2^{p-1}$ plus the remainder $R = k_i - (2^p - 1)$. Every integer in $[0, k_i]$ is uniquely expressible as a subset sum of these $\approx \log_2 k_i$ bundles! Running standard 0/1 Knapsack on these bundles reduces time to $O(W \sum \log k_i)$.
Method 2 (Monotonic Deque per Residue Class $\bmod w_i$): For a fixed item $(w_i, v_i, k_i)$, transition $dp_{new}[w]$ only depends on capacities congruent to $w \pmod{w_i}$. Writing $w = q \cdot w_i + r$, we have:

$$dp_{new}[q w_i + r] = q \cdot v_i + \max_{q - k_i \le j \le q} \big(dp_{old}[j w_i + r] - j \cdot v_i\big)$$

This is a sliding-window maximum of width $k_i + 1$ over $q = 0, 1, \dots, \lfloor (W-r)/w_i \rfloor$, solvable in exact $O(W)$ time per item ($O(N W)$ total) using a monotonic deque!

DP State DAG, 0/1 Knapsack Reverse Sweep & Myers Diff
O(N·W) / O(N·D) Edit Graph

Observe why 0/1 Knapsack iterates capacity w downwards (preventing item reuse), how Patience Sorting finds LIS in O(N log N), and how Myers Diff slides along diagonal snakes k = x - y.

Step 1 / 8
Custom Input:
Timeline

Invariant & State

Item 1 (wt=3, val=4): Sweep w = 7 downTo 3 so dp[w - 3] always reads the previous item layer.
🔗 Live Pointer & State Transition Graph — DP State DAG, 0/1 Knapsack Reverse Sweep & Myers Diff Active Pointer Jump/Transition Matched/Optimal
w=0 0 [0] 0 [1] 0 [2] wt=3, v 4 [3] 4 [4] 4 [5] 4 [6] dp[7]=4 4 [7]
[0]0w=0
[1]0
[2]0
[3]4wt=3, val=4
[4]4
[5]4
[6]4
[7]4dp[7]=4
Item 1
weight=3, value=4
Sweep Direction
w = 7 → 3 (Reverse)
Best dp[7]
4
[Step 1] Initialized 1D table with Item 1 (wt=3, val=4) across capacities 3..7.

2. The $\sqrt{S}$ Frequency Compression Trick for Subset Sum ($\sum w_i = S$)

When the sum of all item weights is $\sum_{i=1}^N w_i = S$, at most $O(\sqrt{S})$ distinct weight values can exist! Why? Because the minimum sum of $d$ distinct positive integers is $1 + 2 + \dots + d = d(d+1)/2 \le S \implies d \le \sqrt{2S}$. By grouping identical weights into (weight, count) pairs and applying binary splitting (or packing pairs of equal weights into a next-level weight $2w$ until at most 2 copies of each weight remain), Subset Sum on $\sum w_i = S$ runs in $O\!\left(\frac{S \sqrt{S}}{64}\right)$ time!

flowchart LR Count["Bounded Item (w, v, k=13)"] --> Split["Binary Splitting:
Bundles of size 1, 2, 4, and remainder 6
(1 + 2 + 4 + 6 = 13)"] Split --> ZeroOne["Every count c in [0..13] can be formed
as a 0/1 subset of {1, 2, 4, 6}!"]

Hardware Reality — AVX2 / AVX-512 Word-Level Bitset Parallelism

In C++, std::bitset<100001> dp; dp |= (dp << w); compiles under -O3 -march=native to a tight loop of 256-bit AVX2 (VPXOR/VPOR/VPSLLQ) or 512-bit AVX-512 instructions processing 256 to 512 capacity states per CPU clock cycle. For $N = 2000, W = 100{,}000$, scalar DP executes $2 \times 10^8$ loop iterations (~150 ms), whereas std::bitset executes $2000 \times (100000 / 256) \approx 7.8 \times 10^5$ SIMD instructions in under 1.2 milliseconds.

3. Complexity Matrix

Knapsack Problem Variant 1D Loop Direction Time Complexity Space Complexity Key Technique
0/1 Knapsack ($c_i \in \{0, 1\}$) Backward ($W \to w_i$) O(N * W) O(W) In-place 1D rolling array
Unbounded Knapsack ($c_i \ge 0$) Forward ($w_i \to W$) O(N * W) O(W) Reuse updated $dp[w - w_i]$
Bounded Knapsack (Binary Split) Backward per bundle O(W * sum log k_i) O(W) Powers-of-2 + remainder bundles
Bounded Knapsack (Monotonic Deque) Residue classes $\bmod w_i$ O(N * W) O(W) Sliding window max on $dp[j w_i + r] - j v_i$
Subset Sum Bitset ($sum w_i = S$) Word-parallel |= O(S sqrt(S) / 64) O(S / 64) Frequency grouping + std::bitset

4. Reference C++20 Implementation

#include <algorithm>
#include <bitset>
#include <cstdint>
#include <deque>
#include <vector>

struct BoundedItem {
    int weight;
    long long value;
    int count;
};

// 1. Bounded Knapsack in O(W * sum(log k_i)) via Binary Splitting
long long bounded_knapsack_binary_split(const std::vector<BoundedItem>& items, int capacity) {
    std::vector<long long> dp(capacity + 1, 0);

    auto apply_01_item = [&](int w, long long v) {
        for (int cap = capacity; cap >= w; --cap) {
            dp[cap] = std::max(dp[cap], dp[cap - w] + v);
        }
    };

    for (const auto& item : items) {
        int rem = item.count;
        for (int b = 1; b <= rem; b <<= 1) {
            int bundle_w = b * item.weight;
            if (bundle_w <= capacity) {
                apply_01_item(bundle_w, b * item.value);
            }
            rem -= b;
        }
        if (rem > 0 && rem * item.weight <= capacity) {
            apply_01_item(rem * item.weight, rem * item.value);
        }
    }
    return dp[capacity];
}

// 2. Bounded Knapsack in Strict O(N * W) Time via Monotonic Deque over Residue Classes
long long bounded_knapsack_monotonic_queue(const std::vector<BoundedItem>& items, int capacity) {
    std::vector<long long> dp(capacity + 1, 0);
    std::deque<std::pair<int, long long>> dq; // (quotient q, shifted_val = dp[q*w + r] - q*v)

    for (const auto& item : items) {
        const int w = item.weight;
        const long long v = item.value;
        const int k = item.count;
        if (w > capacity || k == 0) continue;

        for (int r = 0; r < w; ++r) {
            dq.clear();
            int max_q = (capacity - r) / w;
            for (int q = 0; q <= max_q; ++q) {
                long long shifted = dp[q * w + r] - q * v;
                while (!dq.empty() && dq.back().second <= shifted) {
                    dq.pop_back();
                }
                dq.emplace_back(q, shifted);
                // Evict window elements older than q - k
                if (dq.front().first < q - k) {
                    dq.pop_front();
                }
                dp[q * w + r] = dq.front().second + q * v;
            }
        }
    }
    return dp[capacity];
}

// 3. O(N * W / 64) Word-Parallel Subset Sum using std::bitset
template <std::size_t MAXW = 100001>
bool subset_sum_bitset(const std::vector<int>& weights, int target) {
    if (target < 0 || static_cast<std::size_t>(target) >= MAXW) return false;
    std::bitset<MAXW> reachable;
    reachable.set(0);
    for (int w : weights) {
        reachable |= (reachable << w);
    }
    return reachable.test(target);
}

Production Pitfall — Splitting $k_i$ into Powers of 2 Up to $\lceil \log_2 k_i \rceil$ Instead of Remainder $R$

When binary-splitting $k_i = 13$, if you emit powers of two $1, 2, 4, 8$, their sum is $15 > 13$—allowing the solver to illegally take 14 or 15 copies of the item! You must subtract $1, 2, 4, \dots$ while $2^b \le \text{rem}$ ($13 - 1 - 2 - 4 = 6$) and emit the leftover remainder $R = 6$ as the final bundle so the bundle sum equals $1 + 2 + 4 + 6 = 13$ exactly. Also note: when weights $W \le 10^9$ are huge but values $\sum v_i \le 10^5$ are small, invert the DP state and objective: let $dp[v]$ be the minimum weight required to achieve exact value $v$ in $O(N \sum v_i)$ time!

Check yourself Question 1 of 4
Concept Check 0/1 vs. Unbounded Knapsack 1D Loop Direction recall

Why does the 1D space-optimized 0/1 Knapsack iterate capacity $w$ **backward** (`for (int w = W; w >= w_i; --w)`), whereas Unbounded Knapsack iterates $w$ **forward** (`for (int w = w_i; w <= W; ++w)`)?

⚡ Interactive Challenge · Staff Level

Algorithmic Design & Complexity Check

Analyze the invariant, asymptotic bound, and hardware behavior before selecting.

In the strict $O(N \cdot W)$ Bounded Knapsack algorithm using a monotonic queue for an item $(w, v, k)$, why do we partition capacities $0..W$ by their residue $r = \text{cap} \bmod w$ and slide a window over quotients $q = 0, 1, \dots, \lfloor(W-r)/w\rfloor$ of the quantity $dp_{\text{old}}[q \cdot w + r] - q \cdot v$?

Key Takeaways

0/1 Knapsack iterates capacity backward (W down to w_i) so each item is used at most once; Unbounded Knapsack iterates forward (w_i up to W) to allow unlimited reuse.
Binary splitting decomposes count k_i into powers of two 1, 2, 4, ... plus remainder R, reducing Bounded Knapsack to O(W sum log k_i) 0/1 Knapsack steps.
Grouping capacities by residue r = w mod w_i transforms Bounded Knapsack into a sliding-window maximum solvable in O(N * W) via a monotonic deque.
When capacity W is huge (10^9) but total value V is small (10^5), swap state and value: compute the minimum weight needed to achieve value v in O(N * V).
Was this lesson helpful? Spot an issue or have a suggestion? Let us know on GitHub.
Report an issue