Subset Sum: A Fully Polynomial-Time Approximation Scheme
Objective
The sibling approximation-algorithms-vertex-cover concept (Sections 35.1-35.2) and set-covering-and-lp-rounding concept (Sections 35.3-35.4) all deliver approximation algorithms with a fixed ratio baked into the algorithm: 2, or O(lg |X|), or 8/7. Section 35.5 closes the chapter with something qualitatively different — an approximation scheme, where the accuracy is an input parameter ε that the caller dials, and the algorithm's running time is polynomial in both the input size and 1/ε. That is a fully polynomial-time approximation scheme (FPTAS), and Cormen et al. build one for the optimization version of the subset-sum problem: given a set S of positive integers and a target t, find a subset whose sum is as large as possible without exceeding t. The decision version is NP-complete (proved in Section 34.5.5 and covered by the sibling np-completeness-proofs-and-problem-catalog concept), so the route is: start from an exact algorithm that incrementally builds the list of all achievable subset sums — an approach in the same incremental-recurrence spirit as the sibling dynamic-programming-fundamentals concept, but exponential here because the list itself can double at every step — and then make it polynomial by trimming the list, deliberately throwing away any value that another surviving value already approximates closely enough.
Use Cases
- Solving the practical optimization form of subset sum: the source's framing is a truck that can carry at most
tpounds and up tonboxes with weightsx1, ..., xn, and the question is how heavy a load it can take without exceeding the limit. - Reaching for
EXACT-SUBSET-SUMunchanged when the numbers cooperate — the source notes it is genuinely polynomial-time in the special cases wheretis polynomial in|S|or all the numbers inSare bounded by a polynomial in|S|. Only when the values are large does the exponential list growth bite. - Trading accuracy for time on a continuous dial rather than accepting one fixed ratio:
APPROX-SUBSET-SUMtakesεwith0 < ε < 1and guarantees the returned value is within a factor of1 + εof the optimum, so the same code covers "roughly right and fast" and "nearly exact and slower." - Recognizing the FPTAS shape when you meet it elsewhere: a family of algorithms indexed by
ε, with running time polynomial in1/εas well as in the input size — as opposed to a scheme that is only polynomial in the input size for each fixedε. - Learning list trimming as a reusable technique: whenever a state space is a list of numeric values and near-duplicates are harmless, keeping one representative per multiplicative bucket collapses an exponential list into a logarithmic-sized one.
Deep Dive
The exact algorithm: maintain the list of all achievable sums, capped at t
Let Pi denote the set of values obtainable by summing the members of each (possibly empty) subset of {x1, x2, ..., xi}. For S = {1, 4, 5} this gives P1 = {0, 1}, P2 = {0, 1, 4, 5}, and P3 = {0, 1, 4, 5, 6, 9, 10}. The whole exact algorithm rests on one identity:
Pi = P(i-1) ∪ (P(i-1) + xi)
where L + x means the list built by adding x to every element of L — for example, if L = ⟨1, 2, 3, 5, 9⟩ then L + 2 = ⟨3, 4, 5, 7, 11⟩. The other observation that makes the algorithm practical is that once a subset's sum exceeds t, no superset of it can ever be optimal, so it can be discarded immediately.
java// Faithful translation of EXACT-SUBSET-SUM(S, n, t) (CLRS, Section 35.5).
// Li is the sorted list of sums of subsets of {x1..xi} that do not exceed t.
long exactSubsetSum(long[] x, int n, long t) {
List<Long> l = new ArrayList<>(List.of(0L)); // line 1: L0 = <0>
for (int i = 1; i <= n; i++) { // line 2
l = mergeLists(l, addToEach(l, x[i - 1])); // line 3: merge, duplicates removed
removeGreaterThan(l, t); // line 4
}
return l.get(l.size() - 1); // line 5: largest element in Ln
}MERGE-LISTS(L, L') is the merge-sort merge with duplicates dropped, running in time proportional to the combined lengths, so it keeps every Li sorted. By induction on i (Exercise 35.5-1), Li is exactly the sorted list of every element of Pi that is at most t. The catch is the length: |Li| can be as large as 2^i, so EXACT-SUBSET-SUM is exponential-time in general.
TRIM: keep one representative per multiplicative bucket
The key to turning that into an FPTAS is to trim each Li right after building it. The idea: if two values in a list are close to each other, there is no point keeping both when the goal is only an approximate answer. Given a trimming parameter δ with 0 < δ < 1, trimming a list L removes as many elements as possible such that every removed element y still has a surviving element z representing it, meaning z is no greater than y but within a factor of 1 + δ:
y / (1 + δ) ≤ z ≤ y
Because every surviving value was already a real element of the original list, trimming can only make the answer smaller, never illegal — a crucial property for the correctness proof later.
java// Faithful translation of TRIM(L, delta) (CLRS, Section 35.5).
// L must be sorted into monotonically increasing order. Runs in Theta(m) time.
List<Long> trim(List<Long> l, double delta) {
int m = l.size();
List<Long> out = new ArrayList<>();
out.add(l.get(0)); // line 2: L' = <y1>
long last = l.get(0); // line 3
for (int i = 1; i < m; i++) { // line 4: i = 2 to m
long yi = l.get(i);
if (yi > last * (1 + delta)) { // line 5: yi >= last, L is sorted
out.add(yi); // line 6
last = yi; // line 7
}
}
return out; // line 8
}The procedure makes a single increasing pass, appending a value only when it is the first element or when the most recent value placed into the output cannot represent it. The trace below is the source's own trimming example: L = ⟨10, 11, 12, 15, 20, 21, 22, 23, 24, 29⟩ with δ = 0.1. Each token is one element of L, and it disappears from the row at the moment TRIM declines to append it:
The surviving list is L' = ⟨10, 12, 15, 20, 23, 29⟩: 11 is represented by 10, both 21 and 22 by 20, and 24 by 23. Ten elements became six, and every discarded value still has a close, slightly smaller stand-in.
APPROX-SUBSET-SUM: exact algorithm plus a trim per iteration
java// Faithful translation of APPROX-SUBSET-SUM(S, n, t, eps) (CLRS, Section 35.5).
// Requires 0 < eps < 1. Returns a value within a factor of (1 + eps) of optimal.
long approxSubsetSum(long[] x, int n, long t, double eps) {
List<Long> l = new ArrayList<>(List.of(0L)); // line 1: L0 = <0>
for (int i = 1; i <= n; i++) { // line 2
l = mergeLists(l, addToEach(l, x[i - 1])); // line 3
l = trim(l, eps / (2.0 * n)); // line 4: note eps/2n, NOT eps
removeGreaterThan(l, t); // line 5
}
return l.get(l.size() - 1); // lines 6-7: largest value in Ln
}The only structural difference from EXACT-SUBSET-SUM is line 4. The trimming parameter is ε/2n rather than ε precisely because trimming happens n times and the inaccuracies compound: each pass can shave the surviving values by a factor of 1 + ε/2n, so after n passes the damage is (1 + ε/2n)^n, and shrinking the per-pass parameter by 2n is what keeps that product below 1 + ε.
Here is the source's own worked instance: S = ⟨104, 102, 201, 101⟩, t = 308, ε = 0.40, so the trimming parameter is δ = ε/2n = 0.40/8 = 0.05.
| i | line 3 — merge L(i-1) with L(i-1) + xi |
line 4 — trim by 0.05 | line 5 — drop values > 308 |
|---|---|---|---|
1 (x1 = 104) |
⟨0, 104⟩ |
⟨0, 104⟩ |
⟨0, 104⟩ |
2 (x2 = 102) |
⟨0, 102, 104, 206⟩ |
⟨0, 102, 206⟩ |
⟨0, 102, 206⟩ |
3 (x3 = 201) |
⟨0, 102, 201, 206, 303, 407⟩ |
⟨0, 102, 201, 303, 407⟩ |
⟨0, 102, 201, 303⟩ |
4 (x4 = 101) |
⟨0, 101, 102, 201, 203, 302, 303, 404⟩ |
⟨0, 101, 201, 302, 404⟩ |
⟨0, 101, 201, 302⟩ |
Reading the trims: at i = 2, 104 is dropped because it is within a factor of 1.05 of 102. At i = 3, 206 is dropped as within 1.05 of 201, and 407 survives the trim only to be cut by line 5 for exceeding t = 308. At i = 4, 102 is dropped (within 1.05 of 101), 203 is dropped (within 1.05 of 201), and 303 is dropped (within 1.05 of 302).
The procedure returns z = 302. The true optimum is 307 = 104 + 102 + 101, so the answer is within 2% — comfortably inside the promised ε = 40%. That gap between the guarantee and the observed error is typical: the bound is worst-case.
Why it is fully polynomial: the accuracy bound and the list-length bound
Theorem 35.7: APPROX-SUBSET-SUM is a fully polynomial-time approximation scheme for the subset-sum problem. Two things need proving.
The answer is accurate. Both line 4 and line 5 only ever delete elements, never invent them, so every element of Li is a genuine member of Pi — the returned z is truly the sum of some subset of S, and line 5 guarantees z ≤ t. Let y* be the optimal value (the largest element of Pn that is at most t); then z ≤ y*, and what remains is to show y*/z ≤ 1 + ε.
- Exercise 35.5-2 establishes by induction on
ithat for everyy ∈ Piwithy ≤ t, some survivingz ∈ Lisatisfiesy / (1 + ε/2n)^i ≤ z ≤ y— that is, one trim ofε/2nper iteration, compoundeditimes. - Applying it to
y*ati = ngives an element ofLnwithy*/z ≤ (1 + ε/2n)^n, and since the returnedzis the largest element ofLn, the same bound holds for it. - It remains to show
(1 + ε/2n)^n ≤ 1 + ε. The function(1 + ε/2n)^nis increasing inn(Exercise 35.5-3) and approaches its limite^(ε/2), so it is bounded bye^(ε/2) ≤ 1 + ε/2 + (ε/2)². Because0 < ε < 1forces(ε/2)² ≤ ε/2, that last expression is at most1 + ε.
The running time is polynomial in the input size and in 1/ε. After trimming, any two successive elements z and z' of Li must differ by a factor greater than 1 + ε/2n — that is exactly what surviving the trim means. So each list holds the value 0, possibly the value 1, and at most log_(1+ε/2n) t further values, giving a length bound of
log_(1+ε/2n) t + 2 = ln t / ln(1 + ε/2n) + 2 ≤ 2n(1 + ε/2n)·ln t / ε + 2 < 3n·ln t / ε + 2
using 0 < ε < 1 for the last step. That bound is polynomial in 1/ε and in the input size — which is the lg t bits needed to write t plus the bits needed to write S, itself polynomial in n. Since the running time is polynomial in the list lengths, the whole scheme is fully polynomial. That is the crux: trimming converts a list that could hold 2^i values into one holding O(n·ln t / ε) values, and the compounded error stays bounded because the per-trim parameter was divided by 2n up front.
Trade-offs
- A scheme, not an algorithm — unlike the fixed-ratio algorithms in the sibling concepts, the caller chooses the accuracy. That flexibility has a price:
1/εsits in the running-time bound (3n·ln t / ε + 2elements per list), so halving the allowed error roughly doubles the work. Chasing very smallεwalks you back toward the exponential exact algorithm. - The trimmed answer is always an underestimate, never an overestimate — trimming keeps a representative
z ≤ yfor every discardedy, and line 5 enforcesz ≤ t. So the returned value is a genuinely achievable subset sum that never exceeds the target, which is exactly what the truck-loading framing needs. Exercise 35.5-4 asks how to modify the scheme to approximate the smallest subset sum not less thantinstead. - You get a value, not a subset — as written,
APPROX-SUBSET-SUMreturns only the numberz. Recovering which elements sum to it takes extra bookkeeping (Exercise 35.5-5), the same way a DP table gives you the optimal cost before it gives you the optimal solution. - The exact algorithm is sometimes already good enough —
EXACT-SUBSET-SUMruns in polynomial time whenevertis polynomial in|S|or all the input numbers are polynomially bounded. The FPTAS only earns its keep when the numeric values are genuinely large, which is precisely the regime where the binary encoding of the input makes the problem NP-complete in the first place. - The
ε/2nchoice is load-bearing, not cosmetic — trimming withεdirectly would be the obvious implementation and would be wrong:ncompounded trims ofεeach blow far past a factor of1 + ε. The entire correctness argument rests on(1 + ε/2n)^n ≤ 1 + ε, which in turn needs the0 < ε < 1precondition to close.