Video Explanation
Table of Content
Problem statement
You are given a list of non-negative integers, a1, a2, …, an, and a target, S. Now you have 2 symbols + and -. For each integer, you should choose one from + and - as its new symbol.
Find out how many ways to assign symbols to make sum of integers equal to target S.
Example
Constraints
- The length of the given array is positive and will not exceed 20.
- The sum of elements in the given array will not exceed 1000.
- Your output answer is guaranteed to be fitted in a 32-bit integer.
Input
Brute force recursion
Time Complexity: O(2^N)
Space Complexity: O(N)
For every symbol in nums
, we can make 2 choices, +
or -
. This naturally falls into a tree structure with 2 childrens per parent. In my approach, I deducted the choice from the target
at each level of the tree. You can also introduce an extra variable to accumulate the running sum instead of deducting. If the path from the root of the tree to the leaf gives me a target
of 0, I know I have successfully found a valid arrangement of choices to reach the target
, so I can return 1. Else, it is invalid, so I return 0.
Base Case
At the end of nums
if my remaining target
is 0, return
1 for indicating a good path, 0 otherwise.
Recursion
Sum of paths from my negative choice and my positive choice at the current index.
Complexity Explanation
The time complexity is the number of leaf nodes in our tree, which is O(2^(N+1)
or O(2^N)
. The space complexity is the depth of our tree, which is O(N+1)
or O(N)
2D Dynamic Programming
Time Complexity: O(N*sum(nums))
Space Complexity: O(N*sum(nums))
Now I will show you how you can directly convert the above recursive approach into an iterative DP solution.
If you noticed, the above recursion approach has 2 variables that keeps on changing, index
and target
The goal of the DP is to precompute a table of index
and target
, so that you can avoid keep branching on sub-problems that you have already solved in the decision tree. Refer to the red color nodes in the diagram above, those sub-problems are being solved more than once, so the table allows to fill in the answer for that sub-problem once and then we can access the solution as many times as we need to without having to re-solve it.
To do this, we need a 2D table consist of index
and target
values.
index
should be straightforward as it comes straight from the nums
array, so we only need up to its length.
target
is a bit tricky. Using the given constraints of the problem, we can use 1000
since the sum of nums
can not exceed 1000
, a target > 1000
will never be possible. However, following the same logic, we can achieve the same constraint by just using sum
of nums
. So our target
is limited from -sum(nums)
-> sum(nums)
, -sum(nums)
is for in case all symbols are negative. In code, I called this sum(nums)
, max_limit
Initializing DP table
When we generate our 2D DP table, we can initialize everything to 0 at first to indicate that there is currently 0 paths for the many combinations of index
and target
. Finally, we need to initialize the table with our base case. If we stay consistent with our recursive approach, we want to fill the equivalent of
in our table. We can do so by dp[n][0] = 1
because if we reached the end of nums
and target
is 0, it is a valid path.
BUT WAIT
Because our table represents negative values as well, e.g [-3,-2,-1, 0, 1, 2, 3]
, we need to shift our indices, so our indices don’t go out of bounds. We’ll map it by shifting it by max_limit
, so it becomes within range of [0,1,2,3,4,5,6]
. So we end up with dp[n][0+max_limit] = 1
.
Filling the DP table
When we fill the table, we want to utilize our initialized values and gradually fill the rest of the table from there. Since we have filled dp[n][max_limit]
with a useful value, we want to start filling starting from there.
Inside the TODO
, we want to do the exact equivalent of
so then we have:
See the similarity here? It is a direct translation.
BUT WAIT, WE ARE NOT DONE
Remember how we needed to shift dp[n][0]
? We need to do the same here.
Also because we are iterate through the entire -max_limit
-> max_limit
of our table, centered_j + nums[i]
and centered_j - nums[i]
can easily go out of bounds. However, we can ignore those cases because there is no way for us to reach a target
that is outside of our limits. So we guard out of bounds access by if centered_j + nums[i] >= 0 and centered_j + nums[i] <= 2*max_limit:
and vice versa for centered_j - nums[i]
since our table indices for target
runs from 0 to 2*max_limit+1
.
So after modifying out code with the guard, we would have
Finally, we finished filling up our DP table. Our answer will reside in the equivalent of
, which is dp[0][target+max_limit]
, shifted by max_limit
.
We also need to add a guard for this in case, the target
exceeds our limits again, so
Complexity Explanation
Since we have to generate a 2D table of N
and 2*sum(nums)+1
and also iterate through the entire table, we will have both time complexity and space complexity of O(N*(2*sum(nums)+1))