LeetCode 3539 - Find Sum of Array Product of Magical Sequences
The expression in the statement should be interpreted as: A sequence seq of length m is magical when the binary representation of this sum contains exactly k set bits. Each element of the sequence is an index into nums. The contribution of an index i is the power of two .
Difficulty: 🔴 Hard
Topics: Array, Math, Dynamic Programming, Bit Manipulation, Combinatorics, Bitmask
Solution
Problem Understanding
The expression in the statement should be interpreted as:
$$2^{seq[0]} + 2^{seq[1]} + \cdots + 2^{seq[m-1]}$$
A sequence seq of length m is magical when the binary representation of this sum contains exactly k set bits.
Each element of the sequence is an index into nums. The contribution of an index i is the power of two $2^i$. If an index appears multiple times, the corresponding power of two is added multiple times, which may create binary carries.
For every magical sequence, we compute
$$prod(seq)=\prod nums[seq[i]]$$
and we must return the sum of these products over all magical sequences.
The constraints are the key observation:
m ≤ 30k ≤ 30nums.length ≤ 50
The number of possible sequences is
$$(\text{nums.length})^m$$
which is astronomically large. Therefore, we must count them indirectly using combinatorics and dynamic programming.
An important detail is that the condition depends only on how many times each index appears, not on the order of appearances. However, the product contribution of a count distribution must still include all ordered sequences that realize that distribution.
Several edge cases are important:
- Multiple occurrences of the same index create binary carries.
- The final carry after processing the largest index contributes additional set bits.
nums.lengthcan be much larger thanm, so many indices may never be chosen.kmay be as small as1or as large asm.
Approaches
Brute Force
A brute force solution would generate every sequence of length m.
For each sequence:
- Compute the sum of powers of two.
- Count its set bits.
- If the count equals
k, multiply the corresponding values fromnumsand add the result.
This is correct because it directly follows the definition of a magical sequence.
Unfortunately, the number of sequences is
$$n^m$$
where $n = nums.length$.
With $n=50$ and $m=30$, this is completely infeasible.
Key Insight
Instead of thinking about ordered sequences, think about how many times each index is used.
Let:
$$c_i$$
be the number of times index i appears.
Then:
$$\sum_i c_i = m$$
and the power-of-two sum becomes
$$\sum_i c_i 2^i.$$
The number of ordered sequences corresponding to a count vector is the multinomial coefficient
$$\frac{m!}{\prod_i c_i!}.$$
The product contribution is
$$\prod_i nums[i]^{c_i}.$$
Therefore each count vector contributes
$$m!\prod_i \frac{nums[i]^{c_i}}{c_i!}.$$
Now observe that the binary popcount of
$$\sum_i c_i 2^i$$
can be computed by processing bits from low to high while carrying overflow exactly as binary addition does.
Since m ≤ 30, every count and carry is at most 30, making a digit-DP over positions, carries, used elements, and current popcount feasible.
Approach Comparison
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | $O(n^m \cdot m)$ | $O(m)$ | Enumerates every sequence |
| Optimal | $O(n \cdot m^2 \cdot k \cdot m)$ | $O(n \cdot m \cdot k \cdot m)$ via memoization | DP on counts and binary carries |
Algorithm Walkthrough
- Let
n = len(nums). - Precompute factorials and inverse factorials up to
m.
The contribution of choosing index i exactly c times is
$$\frac{nums[i]^c}{c!}.$$
We precompute these values for every index and every possible count. 3. Define a memoized DP state:
$$dfs(pos,\ remaining,\ carry,\ ones)$$
where:
posis the current index innumsremainingis how many sequence positions still need to be assignedcarryis the incoming binary carryonesis the number of set bits already produced
- At position
pos, choose how many times this index appears:
$$c \in [0, remaining]$$ 5. The total value accumulated at this bit position is
$$carry + c.$$
Therefore:
- current bit
$$bit=(carry+c)\bmod 2$$
- next carry
$$nextCarry=\lfloor (carry+c)/2 \rfloor$$ 6. Add the weight
$$\frac{nums[pos]^c}{c!}$$
and recurse. 7. After all indices have been processed:
- all selections must be used (
remaining == 0) - the remaining carry still contains higher binary digits
The final popcount becomes
$$ones + popcount(carry).$$
8. Accept the state only if this equals k.
9. The DP computes
$$\prod_i \frac{nums[i]^{c_i}}{c_i!}.$$
Multiply the final result by m! to restore the multinomial coefficient.
Why it works
The DP enumerates every possible count vector $(c_0,c_1,\ldots,c_{n-1})$ satisfying $\sum c_i=m$. For each count vector, binary carries are propagated exactly as they would be when evaluating $\sum c_i2^i$, so the computed popcount is correct. The weight accumulated by the DP is $\prod nums[i]^{c_i}/c_i!$, and multiplying by $m!$ converts it into the multinomial count of all ordered sequences realizing that count vector. Thus every magical sequence contributes exactly once and every non-magical sequence contributes zero.
Python Solution
from typing import List
from functools import lru_cache
class Solution:
def magicalSum(self, m: int, k: int, nums: List[int]) -> int:
MOD = 10**9 + 7
n = len(nums)
fact = [1] * (m + 1)
for i in range(1, m + 1):
fact[i] = fact[i - 1] * i % MOD
inv_fact = [1] * (m + 1)
inv_fact[m] = pow(fact[m], MOD - 2, MOD)
for i in range(m, 0, -1):
inv_fact[i - 1] = inv_fact[i] * i % MOD
weight = [[0] * (m + 1) for _ in range(n)]
for i, val in enumerate(nums):
power = 1
for c in range(m + 1):
weight[i][c] = power * inv_fact[c] % MOD
power = power * val % MOD
@lru_cache(None)
def dfs(pos: int, remaining: int, carry: int, ones: int) -> int:
if ones > k:
return 0
if pos == n:
if remaining != 0:
return 0
return 1 if ones + carry.bit_count() == k else 0
res = 0
for cnt in range(remaining + 1):
total = carry + cnt
bit = total & 1
next_carry = total >> 1
res += (
weight[pos][cnt]
* dfs(
pos + 1,
remaining - cnt,
next_carry,
ones + bit,
)
)
res %= MOD
return res
return fact[m] * dfs(0, m, 0, 0) % MOD
The implementation follows the mathematical derivation directly.
The factorial and inverse factorial arrays are used to encode the multinomial coefficient implicitly. The weight table stores
$$nums[i]^c / c!$$
for every index and count.
The memoized DFS processes indices one at a time. For each possible count assigned to the current index, it updates the binary carry and the number of set bits generated at that position.
Once all indices have been processed, the remaining carry contains all higher-order bits. Their popcount is added to the accumulated count, and the state is accepted only when it equals k.
Finally, the answer is multiplied by m!, converting the normalized weights back into the correct multinomial contributions.
Go Solution
package main
func magicalSum(m int, k int, nums []int) int {
const MOD int = 1000000007
n := len(nums)
fact := make([]int, m+1)
invFact := make([]int, m+1)
fact[0] = 1
for i := 1; i <= m; i++ {
fact[i] = int(int64(fact[i-1]) * int64(i) % MOD)
}
powMod := func(a, e int) int {
res := 1
x := a % MOD
for e > 0 {
if e&1 == 1 {
res = int(int64(res) * int64(x) % MOD)
}
x = int(int64(x) * int64(x) % MOD)
e >>= 1
}
return res
}
invFact[m] = powMod(fact[m], MOD-2)
for i := m; i >= 1; i-- {
invFact[i-1] = int(int64(invFact[i]) * int64(i) % MOD)
}
weight := make([][]int, n)
for i := 0; i < n; i++ {
weight[i] = make([]int, m+1)
p := 1
for c := 0; c <= m; c++ {
weight[i][c] = int(int64(p) * int64(invFact[c]) % MOD)
p = int(int64(p) * int64(nums[i]) % MOD)
}
}
type State struct {
pos int
remaining int
carry int
ones int
}
memo := make(map[State]int)
var dfs func(int, int, int, int) int
dfs = func(pos, remaining, carry, ones int) int {
if ones > k {
return 0
}
if pos == n {
if remaining != 0 {
return 0
}
if ones+popcount(carry) == k {
return 1
}
return 0
}
st := State{pos, remaining, carry, ones}
if v, ok := memo[st]; ok {
return v
}
res := 0
for cnt := 0; cnt <= remaining; cnt++ {
total := carry + cnt
bit := total & 1
nextCarry := total >> 1
cur := int(
int64(weight[pos][cnt]) *
int64(dfs(
pos+1,
remaining-cnt,
nextCarry,
ones+bit,
)) % MOD,
)
res += cur
if res >= MOD {
res -= MOD
}
}
memo[st] = res
return res
}
ans := int(int64(fact[m]) * int64(dfs(0, m, 0, 0)) % MOD)
return ans
}
func popcount(x int) int {
cnt := 0
for x > 0 {
cnt++
x &= x - 1
}
return cnt
}
The Go implementation mirrors the Python solution closely. The main differences are the explicit modular exponentiation function, a struct-based memoization key, and careful use of int64 during multiplication to avoid overflow before taking the modulus.
Worked Examples
Example 1
Input:
m = 5
k = 5
nums = [1,10,100,10000,1000000]
The only way to obtain exactly five set bits using five selections is to choose each exponent once:
| Index | Count |
|---|---|
| 0 | 1 |
| 1 | 1 |
| 2 | 1 |
| 3 | 1 |
| 4 | 1 |
Then
$$2^0+2^1+2^2+2^3+2^4=31$$
and
$$popcount(31)=5.$$
The product is
$$1\cdot10\cdot100\cdot10000\cdot1000000=10^{13}.$$
There are
$$5!=120$$
permutations of these indices.
Therefore:
$$120 \cdot 10^{13}$$
modulo $10^9+7$ equals:
991600007
Example 2
Input:
m = 2
k = 2
nums = [5,4,3,2,1]
A pair contributes two set bits exactly when the indices are different.
Examples:
| Sequence | Binary Sum | Popcount |
|---|---|---|
| [0,0] | 2 | 1 |
| [0,1] | 3 | 2 |
| [1,3] | 10 | 2 |
| [4,4] | 32 | 1 |
Thus all ordered pairs with distinct indices are magical.
The total is
$$\sum_{i\neq j} nums[i]nums[j].$$
Using
$$(\sum nums)^2-\sum nums_i^2$$
gives:
$$15^2-55=170.$$
Answer:
170
Example 3
Input:
m = 1
k = 1
nums = [28]
The only sequence is:
[0]
Its power sum is:
$$2^0=1$$
which has one set bit.
Its product is:
$$28.$$
Answer:
28
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | $O(n \cdot m^3 \cdot k)$ | DP states are bounded by position, remaining count, carry, and popcount |
| Space | $O(n \cdot m^2 \cdot k)$ | Memoization stores all reachable states |
The crucial observation is that both the carry and the total number of selected elements are at most 30. This keeps the state space small enough despite nums.length reaching 50.
Test Cases
s = Solution()
assert s.magicalSum(5, 5, [1, 10, 100, 10000, 1000000]) == 991600007 # example 1
assert s.magicalSum(2, 2, [5, 4, 3, 2, 1]) == 170 # example 2
assert s.magicalSum(1, 1, [28]) == 28 # example 3
assert s.magicalSum(1, 1, [7, 9, 11]) == 27 # every single choice valid
assert s.magicalSum(2, 1, [1]) == 1 # repeated index creates carry
assert s.magicalSum(2, 2, [1, 1]) == 2 # two ordered distinct pairs
assert s.magicalSum(3, 1, [2]) == 8 # only one index, heavy carry chain
assert s.magicalSum(3, 3, [1, 1, 1]) > 0 # maximal popcount case
assert s.magicalSum(30, 1, [1]) == 1 # largest m with single index
assert s.magicalSum(4, 2, [3, 5]) >= 0 # mixed carry behavior
| Test | Why |
|---|---|
| Example 1 | Official sample |
| Example 2 | Official sample |
| Example 3 | Official sample |
m=1 with multiple indices |
Every index independently valid |
Single index, m=2 |
Tests carry generation |
| Two indices, equal values | Tests ordered counting |
Single index, m=3 |
Tests repeated carry propagation |
k=m style scenario |
Tests maximum popcount target |
m=30, one index |
Largest allowed m |
| Two-index mixed case | General carry handling |
Edge Cases
Repeated Use of the Same Index
If an index is chosen multiple times, the resulting power of two is added repeatedly. For example, choosing index 0 twice gives:
$$2^0+2^0=2.$$
The result has only one set bit because a carry occurs. A naive solution that simply counts distinct indices would fail. The DP explicitly propagates carries at every bit position, so this case is handled correctly.
Remaining Carry After the Last Index
After processing all indices, there may still be a nonzero carry. For example:
$$30 \cdot 2^0.$$
The binary representation extends beyond the highest index present in nums. Ignoring this carry would produce an incorrect popcount. The implementation adds carry.bit_count() when the recursion reaches the end.
Large nums.length with Small m
The array can contain up to 50 indices while only 30 selections are made. Most indices will often receive count zero. The DP naturally handles this by allowing cnt = 0 at every position, ensuring all valid count distributions are considered without wasting work on impossible selections.