A clear explanation of Combination Sum IV using dynamic programming to count ordered combinations that sum to a target.
Problem Restatement
We are given an array of distinct positive integers nums and an integer target.
We need to count how many possible ordered combinations can add up to target.
Each number in nums can be used multiple times.
Order matters.
That means these are counted as different combinations:
[1, 2, 1]
[2, 1, 1]
[1, 1, 2]Even though they use the same numbers, their order is different.
Input and Output
| Item | Meaning |
|---|---|
| Input | An array nums and an integer target |
| Output | Number of ordered combinations that sum to target |
| Constraint | 1 <= nums.length <= 200 |
| Constraint | 1 <= nums[i] <= 1000 |
| Constraint | All elements in nums are unique |
| Constraint | 1 <= target <= 1000 |
| Guarantee | The answer fits in a 32-bit integer |
Example function shape:
def combinationSum4(nums: list[int], target: int) -> int:
...Examples
Example 1:
nums = [1, 2, 3]
target = 4The possible ordered combinations are:
[1, 1, 1, 1]
[1, 1, 2]
[1, 2, 1]
[1, 3]
[2, 1, 1]
[2, 2]
[3, 1]There are 7 combinations.
So the answer is:
7Example 2:
nums = [9]
target = 3The only number is 9, which is already larger than 3.
There is no way to form 3.
So the answer is:
0First Thought: Backtracking
A direct approach is to try every possible sequence.
At each step, choose one number from nums, subtract it from the remaining target, and continue.
If the remaining target becomes 0, we found one valid sequence.
If it becomes negative, that path fails.
Example:
nums = [1, 2, 3]
target = 4From 4, we may choose 1, 2, or 3.
If we choose 1, the remaining target is 3.
From 3, we again may choose 1, 2, or 3.
This creates a large recursion tree.
Code shape:
class Solution:
def combinationSum4(self, nums: list[int], target: int) -> int:
def dfs(remain: int) -> int:
if remain == 0:
return 1
if remain < 0:
return 0
total = 0
for num in nums:
total += dfs(remain - num)
return total
return dfs(target)This is correct, but it recomputes the same remaining targets many times.
For example, dfs(2) may be reached from several different sequences.
Problem With Brute Force
The brute force recursion branches by every number in nums.
Many paths share the same remaining target.
For example, with:
nums = [1, 2, 3]
target = 4The remaining target 2 can appear after:
[2]
[1, 1]Both paths ask the same question:
“How many ordered combinations form sum 2?”
This repeated work suggests dynamic programming.
Key Insight
Let:
dp[s]mean:
the number of ordered combinations that sum to sWe want:
dp[target]The base case is:
dp[0] = 1There is exactly one way to form sum 0: choose nothing.
Now consider a target sum s.
To build a sequence that sums to s, choose the last number in the sequence.
If the last number is num, then the previous part must sum to:
s - numSo every combination counted in dp[s - num] can become a combination for s by appending num.
Therefore:
dp[s] += dp[s - num]for every num where s >= num.
Algorithm
Create an array:
dp = [0] * (target + 1)Set:
dp[0] = 1Then compute answers for all sums from 1 to target.
For each sum s:
- Try every number
numinnums. - If
num <= s, adddp[s - num]intodp[s]. - After filling the table, return
dp[target].
The loop order matters.
We loop over sums first, then numbers:
for s in range(1, target + 1):
for num in nums:
...This counts ordered combinations.
For example, [1, 3] and [3, 1] are counted separately because they are formed through different last-number choices.
Correctness
We prove that dp[s] stores the number of ordered combinations that sum to s.
The base case is dp[0] = 1. This represents the empty sequence. It is needed so that when s == num, the sequence [num] contributes one valid way.
Now assume all values smaller than s are correct.
For sum s, every valid ordered combination has a last number. Suppose that last number is num.
Then the part before the last number must sum to s - num.
By the assumption, there are exactly dp[s - num] ordered combinations for that previous sum.
Appending num to each of those combinations gives valid ordered combinations for s.
Also, every valid ordered combination for s appears in exactly one group, determined by its last number. So the groups do not overlap.
Therefore summing dp[s - num] over all usable num gives exactly the number of ordered combinations for s.
By induction, dp[target] is correct.
Complexity
Let n = len(nums).
| Metric | Value | Why |
|---|---|---|
| Time | O(target * n) | For each sum, we try every number |
| Space | O(target) | We store one DP value for each sum from 0 to target |
Implementation
class Solution:
def combinationSum4(self, nums: list[int], target: int) -> int:
dp = [0] * (target + 1)
dp[0] = 1
for s in range(1, target + 1):
for num in nums:
if num <= s:
dp[s] += dp[s - num]
return dp[target]Code Explanation
We create a DP table:
dp = [0] * (target + 1)dp[s] means the number of ordered combinations that sum to s.
The base case is:
dp[0] = 1This means there is one way to form sum 0: use no numbers.
Then we fill the table from smaller sums to larger sums:
for s in range(1, target + 1):For each sum, we try every possible last number:
for num in nums:If num can fit inside the current sum:
if num <= s:then every way to form s - num can be extended by appending num:
dp[s] += dp[s - num]Finally, the answer is:
return dp[target]Testing
def test_solution():
s = Solution()
assert s.combinationSum4([1, 2, 3], 4) == 7
assert s.combinationSum4([9], 3) == 0
assert s.combinationSum4([1], 5) == 1
assert s.combinationSum4([2], 3) == 0
assert s.combinationSum4([2, 4, 6], 8) == 7
assert s.combinationSum4([3, 4, 5], 2) == 0
print("all tests passed")
test_solution()Test meaning:
| Test | Why |
|---|---|
[1, 2, 3], target 4 | Main example where order matters |
[9], target 3 | Number larger than target |
[1], target 5 | Only one repeated choice |
[2], target 3 | Cannot form odd target |
[2, 4, 6], target 8 | Multiple ordered combinations with even numbers |
[3, 4, 5], target 2 | All numbers larger than target |