You should aim for a solution as good or better than O(n * m) time and O(n * m) space, where n is the size of the input array and m is the sum of all the elements in the array.
Hint 1
Try to think in terms of recursion and visualize it as a decision tree, where we have two choices at each recursion step: assigning a positive or negative sign.
Hint 2
We recursively iterate through the array using index i, tracking the current sum along the recursive path. Each step branches into two paths, and we sum the number of ways to reach the target. If the index i goes out of bounds, we return 1 if the current sum equals the target; otherwise, we return 0.
Hint 3
This approach is exponential. We can use memoization to cache recursive call results and avoid redundant calculations. A hash map or a 2D array with modifications can be used for caching. If using a 2D array, the dimensions can be (n * (2m + 1)), where n is the array size and m represents the sum of the array elements.
Company Tags
Please upgrade to NeetCode Pro to view company tags.
Prerequisites
Before attempting this problem, you should be comfortable with:
Recursion / Backtracking - Exploring all possible combinations by making choices at each step
Dynamic Programming (Memoization) - Caching results of subproblems defined by index and running sum
Dynamic Programming (Tabulation) - Building solutions bottom-up using hash maps to track reachable sums
1. Recursion
Intuition
This problem asks us to count how many different ways we can assign a + or - sign to each number so that the final sum equals the target.
At every index, we have two independent choices:
add the current number to the total
subtract the current number from the total
Using recursion, we try all possible sign assignments. The recursive function represents: "How many ways can we reach the target starting from index i with the current sum total?"
When all numbers are processed, we simply check whether the accumulated sum equals the target.
Algorithm
Define a recursive function backtrack(i, total):
i is the current index in the array
total is the sum formed so far
If i reaches the end of the array:
Return 1 if total equals the target
Otherwise, return 0
For the current index:
Recurse by adding the current number to total
Recurse by subtracting the current number from total
Add the results of both recursive calls
Start the recursion from index 0 with an initial sum of 0
classSolution:deffindTargetSumWays(self, nums: List[int], target:int)->int:defbacktrack(i, total):if i ==len(nums):return total == target
return(backtrack(i +1, total + nums[i])+
backtrack(i +1, total - nums[i]))return backtrack(0,0)
publicclassSolution{publicintfindTargetSumWays(int[] nums,int target){returnbacktrack(0,0, nums, target);}privateintbacktrack(int i,int total,int[] nums,int target){if(i == nums.length){return total == target ?1:0;}returnbacktrack(i +1, total + nums[i], nums, target)+backtrack(i +1, total - nums[i], nums, target);}}
classSolution{public:intfindTargetSumWays(vector<int>& nums,int target){returnbacktrack(0,0, nums, target);}intbacktrack(int i,int total, vector<int>& nums,int target){if(i == nums.size()){return total == target;}returnbacktrack(i +1, total + nums[i], nums, target)+backtrack(i +1, total - nums[i], nums, target);}};
classSolution{/**
* @param {number[]} nums
* @param {number} target
* @return {number}
*/findTargetSumWays(nums, target){constbacktrack=(i, total)=>{if(i === nums.length){return total === target ?1:0;}return(backtrack(i +1, total + nums[i])+backtrack(i +1, total - nums[i]));};returnbacktrack(0,0);}}
publicclassSolution{publicintFindTargetSumWays(int[] nums,int target){returnBacktrack(0,0, nums, target);}privateintBacktrack(int i,int total,int[] nums,int target){if(i == nums.Length){return total == target ?1:0;}returnBacktrack(i +1, total + nums[i], nums, target)+Backtrack(i +1, total - nums[i], nums, target);}}
funcfindTargetSumWays(nums []int, target int)int{var backtrack func(i int, total int)int
backtrack =func(i int, total int)int{if i ==len(nums){if total == target {return1}return0}returnbacktrack(i+1, total+nums[i])+backtrack(i+1, total-nums[i])}returnbacktrack(0,0)}
class Solution {funfindTargetSumWays(nums: IntArray, target: Int): Int {funbacktrack(i: Int, total: Int): Int {if(i == nums.size){returnif(total == target)1else0}returnbacktrack(i +1, total + nums[i])+backtrack(i +1, total - nums[i])}returnbacktrack(0,0)}}
classSolution{funcfindTargetSumWays(_ nums:[Int],_ target:Int)->Int{funcbacktrack(_ i:Int,_ total:Int)->Int{if i == nums.count {return total == target ?1:0}returnbacktrack(i +1, total + nums[i])+backtrack(i +1, total - nums[i])}returnbacktrack(0,0)}}
Time & Space Complexity
Time complexity: O(2n)
Space complexity: O(n)
2. Dynamic Programming (Top-Down)
Intuition
This problem asks us to count the number of ways to assign a + or - sign to each number so that the final sum equals the target.
The recursive solution tries all possible sign combinations, but many subproblems repeat. To avoid recomputing the same states, we use top-down dynamic programming (memoization).
Each state is uniquely defined by:
the current index i
the current accumulated sum total
The recursive function answers the question: "How many ways can we reach the target starting from index i with the current sum total?"
By caching results for each state, we significantly improve efficiency.
Algorithm
Create a memoization map dp where:
the key is (i, total)
the value is the number of ways to reach the target from that state
Define a recursive function backtrack(i, total):
i represents the current index in the array
total represents the sum formed so far
If i reaches the end of the array:
Return 1 if total equals the target
Otherwise, return 0
If the current state (i, total) is already in dp:
Return the stored value to avoid recomputation
Compute the result by exploring both choices:
Add the current number to total
Subtract the current number from total
Store the computed result in dp[(i, total)]
Start the recursion from index 0 with an initial sum of 0
classSolution:deffindTargetSumWays(self, nums: List[int], target:int)->int:
n =len(nums)
dp =[defaultdict(int)for _ inrange(n +1)]
dp[0][0]=1for i inrange(n):for total, count in dp[i].items():
dp[i +1][total + nums[i]]+= count
dp[i +1][total - nums[i]]+= count
return dp[n][target]
publicclassSolution{publicintfindTargetSumWays(int[] nums,int target){int n = nums.length;Map<Integer,Integer>[] dp =newHashMap[n +1];for(int i =0; i <= n; i++){
dp[i]=newHashMap<>();}
dp[0].put(0,1);for(int i =0; i < n; i++){for(Map.Entry<Integer,Integer> entry : dp[i].entrySet()){int total = entry.getKey();int count = entry.getValue();
dp[i +1].put(total + nums[i],
dp[i +1].getOrDefault(total + nums[i],0)+ count);
dp[i +1].put(total - nums[i],
dp[i +1].getOrDefault(total - nums[i],0)+ count);}}return dp[n].getOrDefault(target,0);}}
classSolution{public:intfindTargetSumWays(vector<int>& nums,int target){int n = nums.size();
vector<unordered_map<int,int>>dp(n +1);
dp[0][0]=1;for(int i =0; i < n; i++){for(auto&p : dp[i]){
dp[i +1][p.first + nums[i]]+= p.second;
dp[i +1][p.first - nums[i]]+= p.second;}}return dp[n][target];}};
classSolution{/**
* @param {number[]} nums
* @param {number} target
* @return {number}
*/findTargetSumWays(nums, target){const n = nums.length;let dp = Array.from({length: n +1},()=>({}));
dp[0][0]=1;for(let i =0; i < n; i++){for(let total in dp[i]){
total =Number(total);let count = dp[i][total];
dp[i +1][total + nums[i]]=(dp[i +1][total + nums[i]]||0)+ count;
dp[i +1][total - nums[i]]=(dp[i +1][total - nums[i]]||0)+ count;}}return dp[n][target]||0;}}
publicclassSolution{publicintFindTargetSumWays(int[] nums,int S){int n = nums.Length;Dictionary<int,int>[] dp =newDictionary<int,int>[n +1];for(int i =0; i <= n; i++){
dp[i]=newDictionary<int,int>();}
dp[0][0]=1;for(int i =0; i < n; i++){foreach(var entry in dp[i]){int total = entry.Key;int count = entry.Value;if(!dp[i +1].ContainsKey(total + nums[i])){
dp[i +1][total + nums[i]]=0;}
dp[i +1][total + nums[i]]+= count;if(!dp[i +1].ContainsKey(total - nums[i])){
dp[i +1][total - nums[i]]=0;}
dp[i +1][total - nums[i]]+= count;}}return dp[n].ContainsKey(S)? dp[n][S]:0;}}
funcfindTargetSumWays(nums []int, target int)int{
n :=len(nums)
dp :=make([]map[int]int, n+1)for i :=0; i <= n; i++{
dp[i]=make(map[int]int)}
dp[0][0]=1for i :=0; i < n; i++{for total, count :=range dp[i]{
dp[i+1][total+nums[i]]+= count
dp[i+1][total-nums[i]]+= count
}}return dp[n][target]}
class Solution {funfindTargetSumWays(nums: IntArray, target: Int): Int {val n = nums.size
val dp =Array(n +1){ mutableMapOf<Int, Int>()}
dp[0][0]=1for(i in0 until n){for((total, count)in dp[i]){
dp[i +1][total + nums[i]]= dp[i +1].getOrDefault(total + nums[i],0)+ count
dp[i +1][total - nums[i]]= dp[i +1].getOrDefault(total - nums[i],0)+ count
}}return dp[n][target]?:0}}
classSolution{funcfindTargetSumWays(_ nums:[Int],_ target:Int)->Int{let n = nums.count
var dp =Array(repeating:[Int:Int](), count: n +1)
dp[0][0]=1for i in0..<n {for(total, count)in dp[i]{
dp[i +1][total + nums[i],default:0]+= count
dp[i +1][total - nums[i],default:0]+= count
}}return dp[n][target,default:0]}}
Time & Space Complexity
Time complexity: O(n∗m)
Space complexity: O(n∗m)
Where n is the length of the array nums and m is the sum of all the elements in the array.
4. Dynamic Programming (Space Optimized)
Intuition
We want to count the number of ways to assign + and - signs to the numbers so that their final sum equals the target.
In the bottom-up DP approach, we used a separate data structure for each index. However, at each step, the new states depend only on the previous step, not on all earlier steps.
This means we can reuse a single data structure to keep track of all possible sums and how many ways each sum can be formed, updating it as we process each number.
Algorithm
Create a map dp where:
the key represents a possible sum
the value represents the number of ways to form that sum
Initialize dp[0] = 1 since there is exactly one way to reach sum 0 before using any numbers
For each number in the array:
Create a new map next_dp to store updated sums
For every (total, count) in dp:
Add the current number → update next_dp[total + num]
Subtract the current number → update next_dp[total - num]
Replace dp with next_dp after processing the current number
After all numbers are processed, dp[target] gives the number of valid ways
Where n is the length of the array nums and m is the sum of all the elements in the array.
Common Pitfalls
Incorrect Memoization Key
When using top-down DP, the state must include both the current index and the running sum. Forgetting to include one of these in the memoization key will cause incorrect caching and wrong results. The key should be (index, total) not just index or just total.
Not Handling Negative Sums in Array-Based DP
When using an array instead of a hashmap for memoization, the running sum can become negative. You must offset the sum by the total sum of all elements to convert it to a valid non-negative index. For example, use dp[i][total + totalSum] where totalSum is the sum of all numbers.
Modifying Map While Iterating (Space-Optimized DP)
In the space-optimized bottom-up approach, you cannot update dp while iterating over it. You must create a new map next_dp for the next state and then replace dp with next_dp after processing each number. Modifying dp in place leads to using updated values in the same iteration.