Topics › Recursion and Backtracking › Subsequences Pattern
Count all subsequences with sum K
Java source ↗ Edit this article ↗
Jump to code:
Problem
You are given an array nums of n integers and an integer k. Count how many subsequences of nums have elements that add up to exactly k.
A subsequence is what remains after deleting zero or more elements without changing the order of the rest. Equivalently, every index is either kept or dropped, so an array of n elements has exactly 2^n subsequences. Two subsequences that come from different index sets are counted separately even if they contain the same values: in [1, 2, 1] the two single 1s are two different subsequences. The empty subsequence has sum 0, so with k = 0 it is counted; if your judge wants it excluded, subtract one from the answer.
Constraints: 0 ≤ n ≤ 20 for the exponential approaches, values and k fit in a 32-bit signed integer, values may be negative.
Example 1
nums = [1, 2, 1], k = 2
answer = 2 // {1, 1} (indices 0 and 2) and {2}
Example 2
nums = [1, 2, 3, 4, 5], k = 5
answer = 3 // {5}, {1, 4}, {2, 3}
Example 3
nums = [1, 2, 3], k = 7
answer = 0 // the whole array only sums to 6
Approach 1: Brute force, enumerate every subsequence
Intuition. There are 2^n subsequences and a bitmask of n bits names each one: bit i set means “index i is kept”. Counting the masks 0 .. 2^n - 1, summing the chosen elements for each and comparing with k is the literal definition of the problem. It is slow because every mask is rebuilt from scratch, n work per mask, even though neighbouring masks share almost all of their elements.
Algorithm.
- For every
maskfrom0to2^n - 1: - Add up
nums[i]for every bitithat is set inmask. - If the sum equals
k, increment the counter. - Return the counter.
static long bruteForce(int[] nums, int k) {
int n = nums.length;
long count = 0;
for (int mask = 0; mask < (1 << n); mask++) {
long sum = 0;
for (int i = 0; i < n; i++) {
if ((mask & (1 << i)) != 0) sum += nums[i];
}
if (sum == k) count++;
}
return count;
}
Complexity. Time O(2^n · n): 2^n masks, each summed in O(n). Space O(1): a counter and a running sum.
The sum is accumulated in a long. Twenty values near 2 × 10^9 add up to 4 × 10^10, far outside int, and a wrapped sum could accidentally equal k.
Approach 2: Optimal, pick / not-pick recursion that returns a count
Intuition. Instead of naming a subsequence by a finished mask, build it one decision at a time. Standing at index i with target still to be collected, there are two moves: take nums[i], which turns the problem into “subsequences of nums[i+1..] summing to target - nums[i]”, or leave it, which turns it into “subsequences of nums[i+1..] summing to target”. The two sub-problems describe disjoint sets of subsequences (one set contains index i, the other does not) and together they cover everything, so the answer is simply their sum. At the end of the array nothing is left to decide: the one subsequence we built is valid exactly when target has been reduced to 0.
This is the general “count the subsequences with property P” pattern from the subsequence theory lecture: the base case returns 1 or 0, and each internal node returns pick + notPick. The same skeleton with || instead of + answers the yes/no version, and with list building instead of counting it prints the subsequences.
Algorithm.
countRec(i, target)answers: how many subsequences ofnums[i..n-1]sum totarget?- Base case
i == n: return1iftarget == 0, otherwise0. pick = countRec(i + 1, target - nums[i]).notPick = countRec(i + 1, target).- Return
pick + notPick. The top-level call iscountRec(0, k).
static long optimal(int[] nums, int k) {
return countRec(nums, 0, k);
}
static long countRec(int[] nums, int i, long target) {
if (i == nums.length) return target == 0 ? 1 : 0;
long pick = countRec(nums, i + 1, target - nums[i]);
long notPick = countRec(nums, i + 1, target);
return pick + notPick;
}
Dry run on nums = [1, 2, 1], k = 2. Each row is one call; the leaves return 1 only when the remaining target is 0.
| call (i, target) | pick → | notPick → | returns |
|---|---|---|---|
| (0, 2) | (1, 1) | (1, 2) | 1 + 1 = 2 |
| (1, 1) | (2, -1) → (3, -2)=0, (3, -1)=0 | (2, 1) → (3, 0)=1, (3, 1)=0 | 0 + 1 = 1 |
| (1, 2) | (2, 0) → (3, -1)=0, (3, 0)=1 | (2, 2) → (3, 1)=0, (3, 2)=0 | 1 + 0 = 1 |
The two leaves that return 1 correspond to picking indices 0, 2 (the two 1s) and picking index 1 alone (the 2).
Complexity. Time O(2^n): the recursion tree is a full binary tree of depth n, and each node does constant work, so the n factor of the brute force disappears. Space O(n) for the recursion stack.
Pruning when every value is non-negative. If no element is negative the running target can never go back up once it drops below 0, so if (target < 0) return 0 can be added before the recursive calls and whole subtrees vanish. The method above does not include it because the problem allows negative values: with nums = [-1, 1, 2, -2] and k = 0, the subsequence {2, -2} passes through target -2 before coming back to 0, and pruning would miss it. Add the shortcut only after checking the input.
Approach 3: Memoising the recursion
Intuition. Different paths can reach the same state (i, target): in [1, 1, 1, 1] the state “index 2, target 1” is reached both by picking the first element and by picking the second. The count for a state depends only on the state, not on how it was reached, so storing it after the first computation and reusing it afterwards turns the exponential tree into a table of distinct states. This is exactly the step from recursion to dynamic programming; the full tabulation is the subject of the DP problem linked below.
Because values may be negative, the remaining target is not confined to 0..k, so a plain 2D array indexed by target does not work. A list of hash maps, one per index, keyed by the remaining target, handles any range.
Algorithm.
- Create
n + 1empty mapsmemo[i]from remaining target to count. - In
countMemo(i, target): return the base case as before; otherwise look upmemo[i].get(target)and return it if present. - Compute
pick + notPickrecursively, store it inmemo[i], and return it.
static long memoized(int[] nums, int k) {
List<Map<Long, Long>> memo = new ArrayList<>();
for (int i = 0; i <= nums.length; i++) memo.add(new HashMap<>());
return countMemo(nums, 0, k, memo);
}
static long countMemo(int[] nums, int i, long target, List<Map<Long, Long>> memo) {
if (i == nums.length) return target == 0 ? 1 : 0;
Long cached = memo.get(i).get(target);
if (cached != null) return cached;
long result = countMemo(nums, i + 1, target - nums[i], memo) + countMemo(nums, i + 1, target, memo);
memo.get(i).put(target, result);
return result;
}
Complexity. Time O(n · S) where S is the number of distinct remaining targets that actually occur; for non-negative arrays S ≤ k + 1, so the running time is O(n · k). In the worst case (all distinct, widely spread values) S can still be exponential and memoisation buys nothing, which is why the plain recursion is the standard answer for this problem and the DP version is a separate one. Space O(n · S) for the maps plus O(n) stack.
Edge cases and pitfalls
- Empty array.
[]withk = 0returns1(the empty subsequence) and0for any otherk. All three methods agree because the base case is hit immediately. - Zeros.
[0, 0]withk = 0returns4: every one of the four subsequences sums to0. If the problem statement wants only non-empty subsequences, subtract one whenk == 0. - Negative numbers. They are the reason the pruning
target < 0is not in the code and the reason the memo uses hash maps rather than an array indexed by target. - Overflow. Sums of several large values exceed
int. The brute force accumulates inlong, and the recursive target is alongfrom the first subtraction. The test[2000000000, 2000000000, -294967296]withk = -294967296has exactly one valid subsequence;intarithmetic would wrap2000000000 + 2000000000to-294967296and report two. - Return type. The count can reach
2^n; withn = 31that no longer fits anint, so the methods returnlong. - Subsequence vs subarray. A subarray is contiguous, a subsequence is not. Counting subarrays with sum
kis a prefix-sum problem, not a recursion one.
Related problems
- Check if there exists a subsequence with sum K is the same recursion with
||instead of+and an early exit. - Power Set prints the subsequences instead of counting them.
- Learn All Patterns of Subsequences (Theory) lists the print / count / exists / first-match variants of this pattern.
- Count subsets with sum K is the tabulated dynamic-programming version for non-negative arrays.
- Subset sum equal to target (DP- 14) tabulates the yes/no variant.
- Count subarrays with given sum is the contiguous counterpart, solved with prefix sums.