Subset Sum — The Target-Sum Subset Problem, with a Detailed DP Solution
What Is Subset Sum?
Given a set of integers S = {s₁, s₂, ..., sₙ} and a target sum T. The question is: Does there exist a subset of elements from S whose sum is exactly T?
Each element can either be included in the subset or excluded, so the solution is selected from all possible combinations of elements.
Example: S = {3, 1, 5, 9, 12}, T = 9 → answer: yes (one possible subset is {3,1,5}).
Problem Description
Given set S and target sum T, determine:
- whether a subset with sum exactly T exists,
- optionally, find one or all subsets whose sum equals T.
Each element may be included at most once. The subset may be empty if T = 0.
Possible Problem Variants
- Decision problem: Returns a yes/no answer indicating whether a subset with sum T exists.
- Counting problem: Count how many distinct subsets have sum T.
- Construction problem: Find and return one or all subsets with sum T.
- Optimization: Find a subset whose sum is as close as possible to T if T cannot be reached exactly.
Worked Example with Concrete Numbers
Input:
- Set: S = {3, 1, 5, 9, 12}
- Target sum: T = 9
Explanation:
- Possible subsets with sum 9: {3,1,5}, {9}
- Each element is either selected or not selected; elements cannot be split or used more than once.
Output:
- Does a subset with sum T exist? → yes
- Example of one subset: {3,1,5}
Notes and Recommendations
- This is a classic NP-complete problem in the general case, but it is often solved with dynamic programming when the numbers are non-negative and T is not too large.
- Efficient approaches include:
- top-down recursion with memoization (each subproblem is checked only once),
- bottom-up tabulation (filling the DP table from 0 to T),
- bitset optimization for small or moderate values of T,
- the meet-in-the-middle approach for a larger number of elements (n ≤ 40).
- With negative numbers, the problem becomes more complex and requires additional techniques (sum offsets or hash maps).
This problem is an excellent exercise for understanding dynamic programming and overlapping subproblems.
Brute Force (Why It Is Not Enough)
The simplest approach tries all subsets — there are 2ⁿ. This is feasible only for small n (n ≤ 20). A more efficient solution is needed.
Dynamic Programming — The Basic Idea
We define a boolean DP array:
dp[sum] = true if the sum 'sum' can be formed using the elements processed so far
Initially: dp[0] = true (the empty subset gives sum 0); all other dp[x] are initially false. When processing a new number num, we update dp to include all new sums obtained by adding num to sums that were already achievable.
for (int num : S) {
for (int sum = T; sum >= num; --sum) {
dp[sum] = dp[sum] || dp[sum - num];
}
}Why Do We Iterate Backwards (from T to num)?
- If we iterated forwards (from num to T), the new value
dp[x]could affect larger sums in the same iteration and allow the same element to be used multiple times — changing the problem into unbounded knapsack. - Iterating backwards guarantees that the new sum
dp[sum]is obtained only from the old (previous) statesdp[sum - num], so the same element is not used more than once.
Intuitive Explanations and Analogies (Blocks / Weights)
Analogy: Imagine the elements as the weights of blocks or weights on a scale. We have several weights, each with its own weight value. dp[x] = true means “you can make a total weight of x in your backpack.”
- If you have only a weight of 3, you can make totals of 0 (an empty backpack) and 3 (by taking that weight). All other totals are impossible. - When you later get another weight, say 4, you can now make 4 (using that weight alone) and 7 (3+4). - It is important to remember all achievable totals so far (`dp` values), because future weights may let you combine with exactly those values.
Detailed Step-by-Step Examples
Example A — One Element Only: {3}, Target T = 6
Initially: dp[0] = true, the rest are false.
After processing 3:
| sum | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|---|
| dp | true | false | false | true | false | false | false |
Explanation: we can either take weight 3 or leave it, giving totals 0 or 3. Totals 1 and 2 are impossible because we do not have a block with a smaller value.
Example B — Adding a Second Element: {3, 4}, Target T = 7
State before the second element (after 3): dp[0]=T, dp[3]=T.
We process num = 4. New achievable totals are obtained by adding 4 to every previously true state:
- 0 + 4 = 4 → dp[4] = true
- 3 + 4 = 7 → dp[7] = true
| sum | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
|---|---|---|---|---|---|---|---|---|
| dp | true | false | false | true | true | false | false | true |
Conclusion: dp[5] remains false because, for 5 to be possible, dp[1] would have to be true beforehand (then 1+4=5). Since dp[1] = false, 5 cannot be formed.
Example C — Adding a Third Element: {3, 4, 2}, Target T = 6
After the previous steps, dp[4]=true and dp[3]=true. Now process num = 2 (iterate over sums backwards):
- dp[6] |= dp[4] → dp[6] = true (because dp[4] = true and 4+2 = 6)
- dp[5] |= dp[3] → dp[5] = true (because dp[3] = true and 3+2 = 5)
- dp[2] |= dp[0] → dp[2] = true
Final state (up to 6): dp[6]=true → target reached.
Why Do We Store All dp[1..T] States?
We store all intermediate results because we do not know which elements will appear later. Each subsequent number can be combined with any sum already achieved. If we do not store dp[3] now, we will not be able to form 3+2=5 later. Thus, even sums that do not seem useful right now may be essential in later steps.
Why Do We Iterate Backwards? (A More Detailed Explanation)
We use a one-dimensional dp array to optimize memory. If we iterate over sums forwards (from num to T), newly computed values from the same iteration may be reused, which means the same element could be used multiple times (which is not desired). Iterating backwards dp[sum] depends only on the old values dp[sum-num], ensuring that each element is included at most once.
Implementation (C++)
An efficient implementation using O(T) memory:
// Subset Sum (boolean) -- O(n * T) time, O(T) memory
#include <iostream>
#include <vector>
using namespace std;
bool subsetSum(const vector<int>& S, int T) {
vector<bool> dp(T+1, false);
dp[0] = true;
for (int num : S) {
for (int sum = T; sum >= num; --sum) {
dp[sum] = dp[sum] || dp[sum - num];
}
}
return dp[T];
}
int main() {
vector<int> S = {3, 4, 2};
int T = 6;
cout << (subsetSum(S, T) ? "Yes\\n" : "No\\n");
return 0;
}
Code Explanation
dp[sum]indicates whether the sum is possible using the elements processed so far.- The loop
for (int sum = T; sum >= num; --sum)ensures that the new element is not used more than once. - At the end of the function, we return
dp[T]— whether the target sum is possible.
Complexity
- Time complexity: O(n · T)
- Space: O(T) (this can be optimized further in special cases)
Tips and Common Mistakes
- Do Not Iterate Forwards (from
numtoT) — this leads to the error of using an element more than once. - Remember that
dp[0]must always betrue(the empty subset). - If the numbers or T are very large, DP may become slow or memory-intensive. Consider alternative approaches or optimizations (e.g. bitset optimization, meet-in-the-middle for n ≤ 40, or heuristics).
- For counting the number of ways (how many subsets have sum T), the logic is similar, but you must store counts (long long) and watch for overflow or use modulo arithmetic if needed.
Quick Summary (for Students)
- Define
dp[sum]— whether the sum is possible. - Start with
dp[0] = true, and set the rest to false. - For each number
numupdate dp backwards: forsum = T..numperformdp[sum] |= dp[sum-num]. - Finally, check
dp[T].
Visual DP Table (Step by Step)
Example: S = {3, 4, 2}, target sum T = 6. Each row represents the state of dp[] after processing one number.
| Step | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|---|
| Initial state | T | F | F | F | F | F | F |
| After 3 | T | F | F | T | F | F | F |
| After 4 | T | F | F | T | T | F | T |
| After 2 | T | F | T | T | T | T | T |
The visual table clearly shows how each new value of num enables new exact sums, and how
dp[] gradually fills up until we reach the target T = 6.
Intuitive Visual Illustration — Blocks / Weights
Problem Subset Sum can be viewed as placing weights on the left side of a scale. We have a set of available weights (the numbers) and want to see which totals can be made by combining them.
Example: S = {3, 4, 2}, target sum T = 6.
Step 1: Initial State
Initially, we can make only the sum of 0 because we have not placed any weights.
Step 2: Add Weight 3
The only new sum we can form is 3, because 0 is achievable and 0 + 3 = 3.
Step 3: Add Weight 4
We now get the following new possible sums:
- 4 (because 0 + 4 = 4)
- 7 (0 + 3 + 4 = 7, but > T=6, so we ignore it for the target sum)
Step 4: Add Weight 2
Weight 2 adds several more possible sums:
- 2 (0 + 2)
- 5 (3 + 2)
- 6 (4 + 2)
We can now form all sums up to 6, including the target sum T=6.
Conclusion
The visual model with weights helps explain the key idea: all sums that may be needed later must be stored in dp[] while we build the solution. This shows how adding each element “builds” new possible sums from the previous ones.
Subset Reconstruction — Which Numbers Make Up Each Sum?
When we solve Subset Sum using only the boolean DP array dp[sum], we know whether a sum is possible, but do not know which numbers. Therefore, we create a separate structure that stores:
parent[sum] = (previous_sum, added_number)
This lets us “work backwards” from the target sum T, tracking which number formed it and which sum existed before it.
Example
Set: {3, 4, 5}
Target sum: T = 7
How to Fill the parent Table:
| Sum | dp[sum] | parent[sum] | Explanation |
|---|---|---|---|
| 3 | true | (0, 3) | 3 can be formed directly using the number 3 |
| 4 | true | (0, 4) | 4 can be formed directly using the number 4 |
| 5 | true | (0, 5) | 5 can be formed directly using the number 5 |
| 7 | true | (3, 4) | 7 = 3 + 4 → the previously formed sum 3 is used |
□ Reconstructing the Subset for T = 7
Start: 7 parent[7] = (3, 4) → means we took 4 parent[3] = (0, 3) → means we took 3 parent[0] = end
Solution:
C++ Code for Tracking the Subset
vector<bool> dp(T+1, false);
vector<pair<int,int>> parent(T+1, {-1,-1});
dp[0] = true;
for (int num : a) {
for (int sum = T; sum >= num; --sum) {
if (dp[sum - num] && !dp[sum]) {
dp[sum] = true;
parent[sum] = {sum - num, num};
}
}
}
// Reconstruction
vector<int> subset;
int cur = T;
while (cur != 0 && parent[cur].first != -1) {
subset.push_back(parent[cur].second);
cur = parent[cur].first;
}
Visual Illustration of Reconstruction
7 ⬑ we added 4
3 ⬑ we added 3
0 (start)
This means the DP algorithm found that 7 can be formed through the chain:
Problem Statement — Subset Sum
Given an array of n positive integersnums[] and a target sum
T.
Determine whether there exists a subset of array elements (each element can be used at most once) whose elements sum exactly to
T.
If such a subset exists, print YES, otherwise print
NO.
Input
- The first line contains two integers
nandT - The second line contains
npositive integers — the array elements
Output
- Print
YESif a subset with sumT - Otherwise, print
NO
Note
- Each array element can be used at most once
- The task only asks whether a solution exists; it does not require printing the subset itself
Solution — Subset Sum (DP with Memoization)
This section contains a complete guide and a fully explained solution for the problem “Subset Sum”. The goal is to deepen understanding of recurrence relations, memoization, and tabulation, which are core dynamic programming techniques.
NP-Completeness and Limitations of the DP Solution
The Subset Sum problem is known to be NP-complete in the general case.
Wikipedia explains the details. This means that, although the DP solution runs in O(n·T) (where n is the number of elements and T is the target sum), this is pseudopolynomial.
Pseudopolynomial means that the running time depends on the numeric values, not only on how many numbers there are. DP is very efficient for small numbers and moderate target sums. However, if the numbers and T are large, the running time may become impractical, and DP does not guarantee polynomial time in the general case.
Alternatives and Optimizations for Larger Values
When dealing with large numbers or a large T, more advanced techniques are available:
- Meet-in-the-middle algorithm — splits the set into two halves and combines their sums; its complexity is
O(2^{n/2}), often practical forn ≈ 40. - Heuristics and approximations — for problems where an exact solution is not required, these can provide faster answers.
- Pseudopolynomial optimization — some DP variants can reduce memory use or running time, but still depend on the numeric values.
For more details and formal results, see arXiv: Pseudopolynomial Subset Sum Algorithms.
Conclusion: The DP solution to Subset Sum is very useful, but students and competitors should be aware of its limitations — it is not universally fast for every input.
Visualizing the Pseudopolynomial Growth of Subset Sum DP
The following table shows an example of how the DP array dp[0..T] grows as we increase the target sum and add numbers. Colors indicate whether a sum is possible (green) or not (red). This illustrates why DP becomes impractical when the numbers are large.
| Numbers | T = 0 | T = 1 | T = 2 | T = 3 | T = 4 | T = 5 | T = 6 |
|---|---|---|---|---|---|---|---|
| Initially | ✔ | ✖ | ✖ | ✖ | ✖ | ✖ | ✖ |
| + Number 1 | ✔ | ✔ | ✖ | ✖ | ✖ | ✖ | ✖ |
| + Number 3 | ✔ | ✔ | ✖ | ✔ | ✖ | ✖ | ✔ |
| + Number 4 | ✔ | ✔ | ✖ | ✔ | ✔ | ✔ | ✔ |
Legend: ✔ the sum is achievable with the current subset; ✖ the sum is not achievable. This table shows how adding numbers makes more target sums achievable, while also illustrating how memory use grows with T.
When T and the elements are large (e.g. T > 10⁵), the DP array becomes huge — even though the solution is correct, it is not practical.
Subset Sum — Visual Illustration of Subsets (Blocks / Weights)
The following illustration shows how different subsets of numbers can form particular target sums T. Each “block” represents a number in the set, and the rows show combinations that contribute to the sums.
Example set:nums = [1, 3, 4], target T = 6
| Subset | Sum = 0 | Sum = 1 | Sum = 2 | Sum = 3 | Sum = 4 | Sum = 5 | Sum = 6 |
|---|---|---|---|---|---|---|---|
| { } | ✔ | ✖ | ✖ | ✖ | ✖ | ✖ | ✖ |
| {1} | ✔ | ✔ | ✖ | ✖ | ✖ | ✖ | ✖ |
| {3} | ✔ | ✖ | ✖ | ✔ | ✖ | ✖ | ✖ |
| {4} | ✔ | ✖ | ✖ | ✖ | ✔ | ✖ | ✖ |
| {1,3} | ✔ | ✔ | ✖ | ✔ | ✖ | ✔ | ✖ |
| {1,4} | ✔ | ✔ | ✖ | ✖ | ✔ | ✔ | ✖ |
| {3,4} | ✔ | ✖ | ✖ | ✔ | ✔ | ✖ | ✔ |
| {1,3,4} | ✔ | ✔ | ✖ | ✔ | ✔ | ✔ | ✔ |
Legend: ✔ the sum is achievable with the given subset; ✖ it is not. Each row shows how combinations of elements contribute to forming the target sum T.
This illustration helps build an intuitive understanding of why DP fills the dp[sum] field from zero to T and combines previously stored results.
Subset Sum — Subset Reconstruction
In practical problems, it is often not enough to determine only whether the target sum T is possible. We may also need to find a subset that forms that sum. This can be done by storing, alongside the DP table, information about whether an element
really had to be selected.
Solution Idea
- We create a DP table
dp[i][sum]which indicates whether sumsumis achievable using the firstielements. - The auxiliary table
take[i][sum]is set totrueonly if taking the element is the only way to obtain that sum. - After filling the DP table, we start from
dp[n][T]and move backwards, usingtaketo reconstruct one valid subset.
C++ Code — Subset Reconstruction
// Subset Sum with reconstruction of one subset
#include <iostream>
#include <vector>
using namespace std;
int main() {
int n, T;
cin >> n >> T;
vector<int> nums(n);
for(int i = 0; i < n; i++) cin >> nums[i];
vector<vector<bool>> dp(n+1, vector<bool>(T+1, false));
vector<vector<bool>> take(n+1, vector<bool>(T+1, false));
dp[0][0] = true;
for(int i = 1; i <= n; i++) {
for(int sum = 0; sum <= T; sum++) {
// case: do not take nums[i-1]
if(dp[i-1][sum]) {
dp[i][sum] = true;
}
// case: take nums[i-1] (but only if this is the only way)
if(sum >= nums[i-1] && dp[i-1][sum - nums[i-1]] && !dp[i-1][sum]) {
dp[i][sum] = true;
take[i][sum] = true;
}
}
}
if(!dp[n][T]) {
cout << "No solution\n";
return 0;
}
// subset reconstruction
vector<int> subset;
int sum = T;
for(int i = n; i > 0; i--) {
if(take[i][sum]) {
subset.push_back(nums[i-1]);
sum -= nums[i-1];
}
}
cout << "Subset with sum " << T << ": ";
for(int x : subset) cout << x << " ";
cout << "\n";
return 0;
}
Test Examples and Edge Cases
- Example where a solution exists:
nums = [1, 3, 4], T = 5→ subset:{1,4}. - Example where no solution exists:
nums = [2,5,7], T = 4→ printsNo solution. - Example where T is larger than the sum of all elements:
nums = [1,2,3], T = 10→ no solution exists. - Example with duplicates:
nums = [1,2,2,3], T = 4→ the subset can be{2,2}or{1,3}.
This implementation guarantees that the reconstructed subset is always consistent with the DP table. The take matrix is used only for decisions that were necessary, avoiding inconsistent reconstructions.
How Subset Reconstruction Works (Visual Explanation)
After the DP table dp has been filled, we know whether the target sum T is achievable, but we still do not know which elements were used. Therefore, we use the auxiliary table take.
The key idea is this:
take[i][sum] = truemeans that sumsumcannot be obtained without taking the elementnums[i-1].
If the sum could also be obtained without that element, take[i][sum] remains false, even if taking it was also possible.
Example
Suppose the following numbers are given:
nums = [1, 3, 4]
T = 5
DP Table (1 = Possible)
sum →
i ↓ 0 1 2 3 4 5
-------------------
0 1 0 0 0 0 0
1 (1) 1 1 0 0 0 0
2 (3) 1 1 0 1 1 0
3 (4) 1 1 0 1 1 1
Marked take decisions
Let us see how dp[3][5]:
-
Without the number
4:dp[2][5] = false -
With the number
4:dp[2][1] = true
Since no solution exists without the number 4, we set:
take[3][5] = true
Backward Traversal (Reconstruction)
Start: (i=3, sum=5)
|
|-- take[3][5] = true → take 4
|
(i=2, sum=1)
|
|-- take[2][1] = false → do not take 3
|
(i=1, sum=1)
|
|-- take[1][1] = true → take 1
|
(i=0, sum=0) ✓
The reconstructed subset is:
{1, 4}
Why Does This Always Work?
takeis set only when the decision was unavoidable.- During reconstruction, we never enter a state that is not supported by the DP table.
- We always obtain a valid subset without contradictions.
This approach provides a clear and reliable way to obtain, in addition to knowing whether a solution exists, a concrete example of the solution.
Variants of the Subset Sum Problem
1. Subset Sum — The Basic Problem (YES / NO)
Problem Statement
Given an array nums[ ] of n positive integers and a target sum
T. Determine whether there is a subset of array elements whose sum is exactly T. Each element can be used at most once. If one exists, print YES, otherwise NO.
Example
Input:
5 9
3 34 4 12 5
Output:
YES
Explanation:
The subset {4, 3, 2} has a sum of 9.
2. Subset Sum — Reconstructing a Solution
Problem Statement
In addition to the YES/NO answer, return the actual subset of element indices that produces the sum
T. If multiple solutions exist, return any one of them. If none exists, print NO.
Example
Input:
5 9
3 34 4 12 5
Output:
YES
Selected indices (1-based): 3 5
Explanation:
nums[3]=4, nums[5]=5 → 4+5=9
3. Subset Sum — How Many Different Subsets Sum to T?
Problem Statement
Calculate the number of distinct subsets of the array nums whose sum is exactly T. Print the count (if overflow is possible, use a 64-bit type or modulo arithmetic).
Example
Input:
4 5
1 2 3 2
Output:
3
Explanation:
Subsets with sum 5: {1,2,2} (using different indices),
{2,3} (different choices of the index for 2) — 3 ways in total.
4. Subset Sum — Minimum Number of Elements
Problem Statement
Find the minimum number of elements in the array nums such that their sum is exactly T. If this is impossible, print -1 (or print a message indicating that no solution exists).
Example
Input:
5 9
3 34 4 12 5
Output:
2
Explanation:
4 + 5 = 9 → minimum number of elements = 2
5. Connection to the 0/1 Knapsack Problem
Problem Statement / Idea
0/1 Knapsack: we have n items with weights w[i] and values v[i], and a knapsack capacity of W. The goal is to maximize the total value without exceeding the total weight of W.
Connection: Subset Sum is a special case of the Knapsack problem. If we set v[i] = w[i] and ask whether we can obtain exactly the value
T, we get Subset Sum. Conversely, Knapsack is a generalization because it allows weights and values to differ.
6. Advanced Variants of Subset Sum
Problem Overview
- Subset Sum with Negative Numbers
- Unlimited use of elements (unbounded)
- Meet-in-the-middle (n ≤ 40)
- Closest sum ≤ T (approximation / closest)
Hidden Mini Quiz — The Subset Sum Problem
Click to test your understanding of the Subset Sum algorithm and the DP approach.
Open Quiz
Mini Quiz: Do You Understand Subset Sum?
1. What does dp[i][s] represent in the Subset Sum problem?
2. What is the DP base case?
3. Why does optimized DP iterate over sums backwards (from T to 0)?
4. What is the time complexity of the standard DP solution for Subset Sum?
5. When is Subset Sum NP-hard in the general case?
| Previous |<DP: Longest Common Subsequence (LCS) |
Next Replacing Iterations with a Formula>| |
