SolvingApproachs:
Approach 1: Brute Force
Algorithm
The brute force approach is based on recursion. We need to try to put both the +
and -
symbols at every location in the given nums array and find out the assignments which lead to the required result S.
For this, we make use of a recursive function calculate(nums, i, sum, S)
, which returns the assignments leading to the sum S, starting from the ith index onwards, provided the sum of elements up to the ith element is sum. This function appends a +
sign and a -
sign both to the element at the current index and calls itself with the updated sum as sum+nums[i] and sum−nums[i] respectively along with the updated current index as i+1. Whenever we reach the end of the array, we compare the sum obtained with S. If they are equal, we increment the count value to be returned.
Thus, the function call calculate(nums, 0, 0, S)
returns the required number of assignments.
Implementation
public class Solution {
int count = 0;
public int findTargetSumWays(int[] nums, int S) {
calculate(nums, 0, 0, S);
return count;
}
public void calculate(int[] nums, int i, int sum, int S) {
if (i == nums.length) {
if (sum == S) {
count++;
}
} else {
calculate(nums, i + 1, sum + nums[i], S);
calculate(nums, i + 1, sum - nums[i], S);
}
}
}
Complexity Analysis
-
Time complexity: O(2n). Size of recursion tree will be 2n. n refers to the size of nums array.
-
Space complexity: O(n). The depth of the recursion tree can go up to n.
Approach 2: Recursion with Memoization
Algorithm
In the last approach, we can observe that a lot of redundant function calls were made with the same value of i as the current index and the same value of sum
as the current sum, since the same values could be obtained through
multiple paths in the recursion tree. In order to remove this
redundancy, we make use of memoization as well to store the results
which have been calculated earlier.
Thus, for every call to calculate(nums, i, sum, S)
, we store the result obtained in memo[i][sum+total], where total
stands for the sum of all the elements from the input array. The factor of total
has been added as an offset to the sum value to map all the sums
possible to positive integer range. By making use of memoization, we
can get the result of each redundant function call in constant time.
Implementation
public class Solution {
int total;
public int findTargetSumWays(int[] nums, int S) {
total = Arrays.stream(nums).sum();
int[][] memo = new int[nums.length][2 * total + 1];
for (int[] row : memo) {
Arrays.fill(row, Integer.MIN_VALUE);
}
return calculate(nums, 0, 0, S, memo);
}
public int calculate(int[] nums, int i, int sum, int S, int[][] memo) {
if (i == nums.length) {
if (sum == S) {
return 1;
} else {
return 0;
}
} else {
if (memo[i][sum + total] != Integer.MIN_VALUE) {
return memo[i][sum + total];
}
int add = calculate(nums, i + 1, sum + nums[i], S, memo);
int subtract = calculate(nums, i + 1, sum - nums[i], S, memo);
memo[i][sum + total] = add + subtract;
return memo[i][sum + total];
}
}
}
Complexity Analysis
-
Time complexity: O(t⋅n). The memo
array of size O(t⋅n) has been filled just once. Here, t refers to the sum of the nums array and n refers to the length of the nums array.
-
Space complexity: O(t⋅n). The depth of recursion tree can go up to n.
The memo
array contains t⋅n elements.
Approach 3: 2D Dynamic Programming
Algorithm
The idea behind this approach is as follows. Suppose we can find out the number of times a particular sum, say sumi is possible up to a particular index, say i, in the given nums array, which is given by say counti. Now, we can find out the number of times the sum sumi+nums[i] can occur easily as counti. Similarly, the number of times the sum sumi−nums[i] occurs is also given by counti.
Thus, if we know all the sums sumj's which are possible up to the jth index by using various assignments, along with the corresponding count of assignments, countj, leading to the same sum, we can determine all the sums possible up to the (j+1)th index along with the corresponding count of assignments leading to the new sums.
Based on this idea, we make use of a dp to determine the number of assignments which can lead to the given sum. dp[i][j] refers to the number of assignments which can lead to a sum of j up to the ith index. To determine the number of assignments which can lead to a sum of sum+nums[i] up to the (i+1)th index, we can use dp[i][sum+nums[i]]=dp[i][sum+nums[i]]+dp[i−1][sum]. Similarly, dp[i][sum−nums[i]]=dp[i][sum−nums[i]]+dp[i−1][sum]. We iterate over the dp
array in a row-wise fashion, i.e., firstly, we obtain all the sums
which are possible up to a particular index along with the corresponding
count of assignments and then proceed for the next element (index) in
the nums array.
But, since the sum can range from -total
to total
, where total
equals to the sum of the nums
array, we need to add an offset of total
to the sum indices (column number) to map all the sums obtained to positive range only.
At the end, the value of dp[n−1][S+total] gives us the required number of assignments. Here, n refers to the number of elements in the nums array.
Implementation
public class Solution {
public int findTargetSumWays(int[] nums, int S) {
int total = Arrays.stream(nums).sum();
int[][] dp = new int[nums.length][2 * total + 1];
dp[0][nums[0] + total] = 1;
dp[0][-nums[0] + total] += 1;
for (int i = 1; i < nums.length; i++) {
for (int sum = -total; sum <= total; sum++) {
if (dp[i - 1][sum + total] > 0) {
dp[i][sum + nums[i] + total] += dp[i - 1][sum + total];
dp[i][sum - nums[i] + total] += dp[i - 1][sum + total];
}
}
}
return Math.abs(S) > total ? 0 : dp[nums.length - 1][S + total];
}
}
Complexity Analysis
-
Time complexity: O(t⋅n). The dp
array of size O(t⋅n) has been filled just once. Here, t refers to the sum of the nums array and n refers to the length of the nums array.
-
Space complexity: O(t⋅n). dp
array of size t⋅n is used.
Approach 4: 1D Dynamic Programming
Algorithm
If we look closely at the last solution, we can observe that to evaluate the current row of dp, only the values of the last row of dp
are needed. Thus, we can save some space by using a 1D DP array instead
of a 2D DP array. The only change we need to make is that we have to
create an array next of the same size as dp so that we can update it while scanning through dp since it is not safe to mutate dp when the iteration is in progress. After the iteration is completed, we set dp equal to next and create a new empty array next before the next iteration starts, and so on.
Implementation
public class Solution {
public int findTargetSumWays(int[] nums, int S) {
int total = Arrays.stream(nums).sum();
int[] dp = new int[2 * total + 1];
dp[nums[0] + total] = 1;
dp[-nums[0] + total] += 1;
for (int i = 1; i < nums.length; i++) {
int[] next = new int[2 * total + 1];
for (int sum = -total; sum <= total; sum++) {
if (dp[sum + total] > 0) {
next[sum + nums[i] + total] += dp[sum + total];
next[sum - nums[i] + total] += dp[sum + total];
}
}
dp = next;
}
return Math.abs(S) > total ? 0 : dp[S + total];
}
}
Complexity Analysis
-
Time complexity: O(t⋅n). Each of the n dp
arrays of size t has been filled just once. Here, t refers to the sum of the nums array and n refers to the length of the nums array.
-
Space complexity: O(t). Two dp
arrays of size 2⋅t+1 are used, therefore the space usage is O(t).
Python Code will be added soon.