Problem. items, item has weight and value . Choose a subset of total weight maximising the total value.
NP-hard, but only weakly β the DP is polynomial in the value of , not in its number of bits. That is exactly why an FPTAS exists.
The DP β
with the weight loop running downward so each item is used once.
long long knapsack(int n, int W, vector<int>& w, vector<long long>& v) {
vector<long long> dp(W + 1, 0);
for (int i = 0; i < n; i++)
for (int cap = W; cap >= w[i]; cap--)
dp[cap] = max(dp[cap], dp[cap - w[i]] + v[i]);
return dp[W];
}time, space. See Knapsack for the full family.
When is huge but the values are small
Invert the DP: index by value, store the minimum weight.
long long V = accumulate(v.begin(), v.end(), 0LL);
vector<long long> dp(V + 1, LLONG_MAX / 2);
dp[0] = 0;
for (int i = 0; i < n; i++)
for (long long val = V; val >= v[i]; val--)
dp[val] = min(dp[val], dp[val - v[i]] + w[i]);
for (long long val = V; val >= 0; val--) if (dp[val] <= W) return val;. Always check which of and is smaller β the constraints usually make one of them tractable, and this inversion is the intended solution surprisingly often.
The decision table
| Situation | Method | Cost |
|---|---|---|
| standard DP | ||
| value-indexed DP | ||
| Boolean (subset sum) | bitset | |
| , both huge | meet in the middle | |
| Items divisible | greedy by | |
| Unbounded copies | weight loop upward | |
| copies each | binary splitting | |
| copies, need | monotone deque per residue class | |
| approximation | scale the values, then DP | |
| Few distinct weights | DP over counts | |
| Two constraints (weight and volume) | dp[w][v] |
The FPTAS
Round the values down to multiples of , then run the value-indexed DP. The rounding loses at most in total.
double K = eps * maxValue / n;
for (auto& item : items) item.v = (long long)(item.v / K);
// value range is now O(n / eps), so the DP is O(n^2 / eps)-approximate in time polynomial in both and β the definition of an FPTAS. Knapsack is the standard example, and it exists precisely because the problem is weakly (not strongly) NP-hard.
Fractional knapsack β the easy cousin
If items may be split, greedy by value density is optimal: take items in that order, splitting the last.
sort(items.begin(), items.end(), [](auto& a, auto& b) {
return (double)a.v / a.w > (double)b.v / b.w; // or a.v * b.w > b.v * a.w
});Compare with cross-multiplication rather than floating-point division to stay exact. The exchange argument is one line: swapping any two out of density order cannot help.
This is the LP relaxation of 0/1 knapsack, and its value is the standard bound for branch and bound.
Reconstruction
// needs the full 2D table
int cap = W;
for (int i = n - 1; i >= 0; i--)
if (dp[i + 1][cap] != dp[i][cap]) { chosen.push_back(i); cap -= w[i]; }With the 1D array this is impossible β that is the cost of the space optimisation. If you need the items, either keep the 2D table or store a take[i][cap] bitset ( bits, which is often affordable).
Why it is worth knowing
Knapsack is the reference point for the whole DP-versus-NP-hardness discussion:
- it is NP-hard, yet has a simple pseudo-polynomial DP;
- the DP can be inverted when the other dimension is smaller;
- it admits an FPTAS, unlike strongly NP-hard problems;
- its fractional relaxation is greedy-solvable and bounds the integral optimum.
Those four facts together are the standard illustration of what βweakly NP-hardβ means in practice.
See also: Knapsack Β· Subset Sum Β· Approximation Algorithms