Topics › Dynamic Programming › DP on Subsequences
Count subsets with sum K
Java source ↗ Edit this article ↗
Jump to code:
Problem
You are given an array arr of n non-negative integers and a target k. Count the subsets whose elements add up to exactly k. Subsets are told apart by indices, not values: in [1, 2, 2] the subsets {arr[0], arr[1]} and {arr[0], arr[2]} are different even though both are {1, 2}. The count can be huge, so return it modulo 10^9 + 7.
Typical constraints: 1 ≤ n ≤ 100, 0 ≤ arr[i] ≤ 1000, 0 ≤ k ≤ 1000. Note that zeros are allowed; that single detail is where most solutions to this problem go wrong.
Example 1
arr = [1, 2, 2, 3], k = 3
answer = 3 // {1, first 2}, {1, second 2}, {3}
Example 2
arr = [0, 0, 1], k = 1
answer = 4 // {1}, {0a, 1}, {0b, 1}, {0a, 0b, 1}: each zero may or may not join
From “is there one” to “how many”
The existence version of this problem asks can(i, t) = can(i - 1, t) OR can(i - 1, t - arr[i]). Counting changes only the combining operator. Every subset of the first i elements either excludes the last of them or includes it, never both, so the two groups are disjoint and their counts simply add:
ways(i, t) = ways(i - 1, t) + ways(i - 1, t - arr[i - 1]) (second term only if arr[i - 1] ≤ t)
This “replace OR by +” step works for any pick / not-pick recursion whose branches produce disjoint sets of outcomes, which is the case whenever the branches differ in a decision about the same element.
The zero trap and the base case
In the existence version, a common base case is “if t == 0, return true”, because the empty subset always works. Copying that into the counting version as “if t == 0, return 1” is wrong when zeros exist. It says there is exactly one way to finish (take nothing more), but each remaining zero can be taken or not, giving 2^(zeros left) ways. On [0, 0, 1] with k = 1 the early exit returns 1 instead of 4.
The fix used throughout the Java file is to index by prefix length and stop only when no elements remain:
ways(0, t) = 1 if t == 0, else 0 // with no elements, only the empty subset exists
answer = ways(n, k)
Zeros are then just ordinary elements: for t unchanged, ways(i, t) = ways(i - 1, t) + ways(i - 1, t - 0), which doubles the count, exactly as it should. (Striver’s version keeps the index-based base case at i == 0 and returns 2 when arr[0] == 0 and t == 0; that is the same fix written differently.)
Approach 1: Recursion
Intuition. count(i, t) is the number of subsets of arr[0..i-1] with sum t. Decide about the last available element arr[i - 1]: skip it, or take it if it fits. Add the two counts, reducing modulo 10^9 + 7.
Algorithm.
- If
i == 0, return1whent == 0, else0. notTake = count(i - 1, t).take = count(i - 1, t - arr[i - 1])ifarr[i - 1] ≤ t, else 0.- Return
(notTake + take) % MOD.
static int recursive(int[] arr, int k) {
return count(arr.length, k, arr);
}
static int count(int i, int t, int[] arr) {
if (i == 0) return t == 0 ? 1 : 0; // no elements left: only the empty subset, with sum 0
int notTake = count(i - 1, t, arr);
int take = arr[i - 1] <= t ? count(i - 1, t - arr[i - 1], arr) : 0;
return (notTake + take) % MOD;
}
Complexity. Time O(2^n): the recursion visits every subset. Space O(n) for the stack.
The sum of two values below MOD is at most 2,000,000,012, which still fits in an int (the limit is 2,147,483,647), so int arithmetic with one % per addition is safe.
Approach 2: Memoization
Intuition. The state is (i, t) with 0 ≤ i ≤ n and 0 ≤ t ≤ k. Many different choices among the later elements leave the same (i, t), and the count from there is the same each time. Caching gives one computation per state.
0 is a legitimate count, so the table is filled with -1 to mean “not computed”.
Algorithm.
- Allocate
dp[n + 1][k + 1]filled with-1. - Same recursion; return
dp[i][t]when known, store it otherwise.
static int memoization(int[] arr, int k) {
int[][] dp = new int[arr.length + 1][k + 1];
for (int[] row : dp) Arrays.fill(row, -1); // counts are >= 0, so -1 = unknown
return memo(arr.length, k, arr, dp);
}
static int memo(int i, int t, int[] arr, int[][] dp) {
if (i == 0) return t == 0 ? 1 : 0;
if (dp[i][t] != -1) return dp[i][t];
int notTake = memo(i - 1, t, arr, dp);
int take = arr[i - 1] <= t ? memo(i - 1, t - arr[i - 1], arr, dp) : 0;
return dp[i][t] = (notTake + take) % MOD;
}
Complexity. Time O(n · k). Space O(n · k) plus O(n) stack.
Approach 3: Tabulation
Intuition. dp[i][t] depends only on row i - 1, so fill rows 1..n in order starting from dp[0][0] = 1 and the rest of row 0 equal to 0. The inner loop must start at t = 0, not t = 1: a zero element adds new subsets with sum 0, so column 0 changes from row to row.
Algorithm.
dp[0][0] = 1.- For
i = 1..nandt = 0..k:dp[i][t] = dp[i - 1][t] + (arr[i - 1] ≤ t ? dp[i - 1][t - arr[i - 1]] : 0), moduloMOD. - Return
dp[n][k].
static int tabulation(int[] arr, int k) {
int n = arr.length;
int[][] dp = new int[n + 1][k + 1];
dp[0][0] = 1; // the empty prefix has exactly one subset: {}
for (int i = 1; i <= n; i++) {
for (int t = 0; t <= k; t++) { // t starts at 0: zeros add subsets with sum 0
int notTake = dp[i - 1][t];
int take = arr[i - 1] <= t ? dp[i - 1][t - arr[i - 1]] : 0;
dp[i][t] = (notTake + take) % MOD;
}
}
return dp[n][k];
}
Dry run on Example 1 (arr = [1, 2, 2, 3], k = 3):
| i (element) | t = 0 | 1 | 2 | 3 |
|---|---|---|---|---|
| 0 (none) | 1 | 0 | 0 | 0 |
| 1 (1) | 1 | 1 | 0 | 0 |
| 2 (2) | 1 | 1 | 1 | 1 |
| 3 (2) | 1 | 1 | 2 | 2 |
| 4 (3) | 1 | 1 | 2 | 3 |
In row 3, t = 2 becomes 2 because there are now two different 2s; t = 3 becomes 2 because each of them pairs with the 1. Row 4 adds {3} itself, giving the answer 3.
Complexity. Time O(n · k), space O(n · k).
Approach 4: Space optimized
Intuition. One array ways holds the current row. For each element x, update ways[t] += ways[t - x] with t going from k down to x. Because t - x < t, the value read has not yet been updated for x, so it still counts subsets without x; each element is therefore used at most once.
Zeros work automatically: with x = 0 the loop runs t from k down to 0 and does ways[t] += ways[t], doubling every entry, which is precisely the effect of an optional zero.
Algorithm.
ways[0] = 1, the rest 0.- For each
xinarr, fortfromkdown tox:ways[t] = (ways[t] + ways[t - x]) % MOD. - Return
ways[k].
static int spaceOptimized(int[] arr, int k) {
int[] ways = new int[k + 1];
ways[0] = 1;
for (int x : arr) {
for (int t = k; t >= x; t--) { // right to left: ways[t - x] still excludes x
ways[t] = (ways[t] + ways[t - x]) % MOD;
}
}
return ways[k];
}
Complexity. Time O(n · k), space O(k).
Edge cases and pitfalls
- Zeros. Covered above. The tests include
[0, 0, 1]withk = 1(answer 4) and[0, 0, 0]withk = 0(answer 8). k = 0. The empty subset counts, so[1, 2, 3]withk = 0gives 1. If your platform excludes the empty subset, subtract 1 fork = 0.- Modulo. Apply
%after every addition. The Java file checks 60 zeros withk = 0, whose true count2^60only fits after reduction. - Duplicates. Do not de-duplicate the input: equal values at different indices form different subsets, as Example 1 shows.
- Loop direction in 1D. Left to right would let an element be used repeatedly (that is the coin-change update), overcounting.
- Negative numbers. Not supported by this state space; they would require an offset for
t.
Related problems
- Subset sum equal to target (DP- 14) is the yes/no version of this recursion.
- Count partitions with given difference reduces to exactly this count.
- Target sum reduces to this count as well, after turning signs into two groups.
- Count all subsequences with sum K is the plain recursion version from the recursion section.