Topics › Recursion and Backtracking › Subsequences Pattern
Check if there exists a subsequence 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. Decide whether some subsequence of nums has elements adding up to exactly k, and return true or false.
A subsequence is obtained by deleting zero or more elements without reordering the rest, so each index is independently kept or dropped and there are 2^n candidates. The empty subsequence has sum 0; with k = 0 the answer is therefore true for every array. Values may be negative.
This is the “does any subsequence satisfy P” member of the subsequence pattern family. Compared with counting, the only change is that we are allowed to stop the moment a single success is found, and that small change is the whole point of the exercise.
Constraints: 0 ≤ n ≤ 20 for the exponential approaches, values and k fit in a 32-bit signed integer.
Example 1
nums = [1, 2, 3], k = 5
answer = true // 2 + 3
Example 2
nums = [1, 2, 3], k = 7
answer = false // the largest possible sum is 1 + 2 + 3 = 6
Example 3
nums = [-5, 3, 8], k = -2
answer = true // -5 + 3
Approach 1: Brute force, enumerate every subsequence
Intuition. Name each subsequence by an n-bit mask (bit i set means index i is kept), sum the chosen elements, and return true the first time a sum equals k. It is correct by definition and simple to write, but every mask is summed from scratch, so each candidate costs O(n) even though consecutive masks differ in very few positions.
Algorithm.
- For every
maskfrom0to2^n - 1: - Add up
nums[i]for every set biti. - If the sum equals
k, returntrue. - After the loop, return
false.
static boolean bruteForce(int[] nums, int k) {
int n = nums.length;
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) return true;
}
return false;
}
Complexity. Time O(2^n · n) in the worst case (the answer is false, or the only match is the last mask). Space O(1).
The running sum is a long: several values near 2 × 10^9 overflow int, and a wrapped sum could accidentally equal k.
Approach 2: Optimal, pick / not-pick with early exit
Intuition. Build the subsequence one decision at a time. At index i with target still to be collected, either take nums[i] (new sub-problem: nums[i+1..] with target - nums[i]) or skip it (nums[i+1..] with target). Some subsequence of nums[i..] sums to target exactly when at least one of the two sub-problems succeeds, so the recursion returns pick || notPick.
The important detail is how that || is evaluated. We make the pick call first and, if it returns true, return immediately without ever exploring the not-pick subtree. In the counting version both subtrees are always needed; here, as soon as one leaf succeeds, every frame on the stack returns true and the remaining tree is skipped. When a solution exists early in the search this is dramatically faster; when the answer is false nothing can be skipped and the full 2^n tree is visited.
Algorithm.
existsRec(i, target)answers: does some subsequence ofnums[i..n-1]sum totarget?- Base case
i == n: returntarget == 0. - If
existsRec(i + 1, target - nums[i])istrue, returntrue. - Otherwise return
existsRec(i + 1, target). The top-level call isexistsRec(0, k).
static boolean optimal(int[] nums, int k) {
return existsRec(nums, 0, k);
}
static boolean existsRec(int[] nums, int i, long target) {
if (i == nums.length) return target == 0;
if (existsRec(nums, i + 1, target - nums[i])) return true; // pick nums[i]; if that works we are done
return existsRec(nums, i + 1, target); // otherwise try leaving nums[i] out
}
Dry run on nums = [1, 2, 3], k = 5. The pick branch is always tried first.
| call (i, target) | action |
|---|---|
| (0, 5) | pick 1 → (1, 4) |
| (1, 4) | pick 2 → (2, 2) |
| (2, 2) | pick 3 → (3, -1): leaf, false; not pick → (3, 2): leaf, false; returns false |
| (1, 4) | not pick 2 → (2, 4) |
| (2, 4) | pick 3 → (3, 1): false; not pick → (3, 4): false; returns false |
| (0, 5) | not pick 1 → (1, 5) |
| (1, 5) | pick 2 → (2, 3) |
| (2, 3) | pick 3 → (3, 0): leaf, true |
The true propagates straight up: (2, 3), (1, 5) and (0, 5) each return without touching their not-pick branch. Of the 15 internal and leaf calls a full tree would make, only 11 were performed; on larger inputs with an early solution the saving is far bigger.
Complexity. Time O(2^n) in the worst case, each node doing O(1) work; often much less thanks to the early exit. Space O(n) for the recursion stack.
Pruning for non-negative arrays. If no value is negative, target only ever decreases, so if (target < 0) return false before the recursive calls cuts off whole subtrees. It is not in the method above because negative values are allowed: with nums = [-5, 3, 8] and k = -2 the very first call already has a negative target and the shortcut would answer false on the spot, yet {-5, 3} sums to -2. Check the input before adding the shortcut.
Approach 3: Memoising repeated states
Intuition. The outcome of existsRec(i, target) depends only on (i, target), not on the path that led there, and with repeated values many paths share states. Caching the boolean per state means every distinct state is solved once, which turns the exponential tree into a table whose size is the number of distinct (index, remaining target) pairs. For non-negative arrays that is at most n · (k + 1), the classic subset-sum DP table. Because targets may be negative here, each index gets a hash map rather than a fixed-size row.
Algorithm.
- Create
n + 1empty maps, one per index, from remaining target to result. - In
existsMemo(i, target): handle the base case; otherwise return the cached value if present. - Compute
pick || notPickrecursively (still short-circuiting), store it, and return it.
static boolean memoized(int[] nums, int k) {
List<Map<Long, Boolean>> memo = new ArrayList<>();
for (int i = 0; i <= nums.length; i++) memo.add(new HashMap<>());
return existsMemo(nums, 0, k, memo);
}
static boolean existsMemo(int[] nums, int i, long target, List<Map<Long, Boolean>> memo) {
if (i == nums.length) return target == 0;
Boolean cached = memo.get(i).get(target);
if (cached != null) return cached;
boolean result = existsMemo(nums, i + 1, target - nums[i], memo) || existsMemo(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 occur; O(n · k) for non-negative input, but potentially still exponential for widely spread distinct values, when the maps simply never hit. Space O(n · S) for the maps plus O(n) stack.
Edge cases and pitfalls
- Empty array.
[]withk = 0istrue(the empty subsequence), any otherkisfalse. k = 0on a non-empty array. Alwaystruebecause of the empty subsequence. If the judge insists on a non-empty subsequence, check for a zero element or for a non-empty subset summing to zero separately.- Forgetting to short-circuit. Writing
boolean a = pick; boolean b = notPick; return a || b;is still correct but always explores the full tree. Return as soon as the pick branch succeeds, or use||directly, which short-circuits in Java. - Negative numbers. Make the
target < 0pruning wrong and make an array-based memo table impossible without an offset; the hash-map memo sidesteps both. - Overflow.
[2000000000, 2000000000]withk = -294967296must returnfalse; adding the two values inintwraps to exactly-294967296and would returntrue. The brute force sums inlongand the recursion carries alongtarget. - Worst case is unavoidable. When the answer is
false(for example twenty2s and an oddk) no early exit happens and the full2^ntree is traversed; only memoisation or DP helps then.
Related problems
- Count all subsequences with sum K is the same recursion with
+instead of||, where early exit is impossible. - Learn All Patterns of Subsequences (Theory) explains the print / count / exists variants side by side.
- Power Set generates every subsequence with the same pick / not-pick skeleton.
- Subset sum equal to target (DP- 14) tabulates this exact question for non-negative arrays.
- Partition equal subset sum reduces to this problem with
k = total / 2. - Two Sum is the special case of exactly two elements, solvable in linear time with a hash map.