Facebook Pixel

2835. Minimum Operations to Form Subsequence With Target Sum

HardGreedyBit ManipulationArray
Leetcode Link

Problem Description

You have an array nums containing only non-negative powers of 2 (like 1, 2, 4, 8, 16, etc.) and a target value target.

You can perform the following operation any number of times:

  • Pick any element nums[i] that is greater than 1
  • Remove that element from the array
  • Add two copies of nums[i] / 2 to the end of the array

For example, if you pick the element 8, you remove it and add two 4's to the array.

Your goal is to find the minimum number of operations needed so that you can select some elements from the resulting array (a subsequence) that sum to exactly target. If it's impossible to achieve this, return -1.

A subsequence means you can pick any elements from the array while maintaining their relative order - you don't need to pick consecutive elements.

The key insight is that each operation splits a larger power of 2 into two smaller equal powers of 2. For instance:

  • Splitting 16 gives you two 8's
  • Splitting 8 gives you two 4's
  • Splitting 4 gives you two 2's
  • Splitting 2 gives you two 1's

Since powers of 2 have unique binary representations (each has exactly one bit set to 1), this problem essentially becomes about manipulating bits. When you split a power of 2, you're moving a bit from a higher position to a lower position in binary representation.

The solution tracks how many 1's appear at each bit position across all numbers, then greedily tries to match the target's binary representation by splitting higher bits when needed.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

Let's think about what happens when we perform an operation. When we split a power of 2, we're essentially breaking down a larger power into smaller ones. For example, splitting 8 (which is 2^3) gives us two 4's (each is 2^2). The total sum remains the same - we had 8 before, and we have 4 + 4 = 8 after.

This immediately tells us something important: if the sum of all elements in nums is less than target, it's impossible to reach the target no matter how many operations we perform. The sum never changes!

Now, since we're dealing with powers of 2, let's think in binary. Each power of 2 has exactly one bit set:

  • 1 = 2^0 = binary 0001
  • 2 = 2^1 = binary 0010
  • 4 = 2^2 = binary 0100
  • 8 = 2^3 = binary 1000

When we split a number, we're moving a bit from position i to position i-1 and creating two copies of it. This is like "breaking a larger bill into smaller denominations" - splitting a 20billintotwo20 bill into two 10 bills.

To build our target sum, we need certain bits to be available. If we look at target in binary, each bit position that's set to 1 requires us to have at least one number with that bit available.

The greedy insight comes from realizing that we should always use the smallest available bit that satisfies our need. Why? Because splitting larger bits is more expensive (requires more operations). If we need a bit at position i and we have it available, we use it directly (0 operations). If we don't have it but have a bit at position j where j > i, we need to split down from position j to position i, which takes j - i operations.

We can also combine smaller bits to form larger ones - two bits at position i can combine to form one bit at position i+1. This is like combining two 10billstomakea10 bills to make a 20 bill.

The algorithm therefore:

  1. Counts how many bits we have at each position across all numbers
  2. For each bit needed in target (from low to high), finds the nearest available bit
  3. Splits down if necessary, keeping track of the operations needed
  4. Combines smaller bits when possible to prepare for future needs

Learn more about Greedy patterns.

Solution Approach

Let's walk through the implementation step by step:

Step 1: Check if solution is possible

s = sum(nums)
if s < target:
    return -1

Since operations don't change the total sum, if the sum of all elements is less than target, it's impossible to form the target.

Step 2: Count bits at each position

cnt = [0] * 32
for x in nums:
    for i in range(32):
        if x >> i & 1:
            cnt[i] += 1

We create an array cnt where cnt[i] represents how many times bit i appears across all numbers. For each number x, we check which bit position has a 1 (since each power of 2 has exactly one bit set).

Step 3: Process target bits from low to high The main loop uses two pointers i and j:

  • i points to the current bit position we need in target
  • j points to the position we're currently processing in cnt
while 1:
    # Skip bits in target that are 0
    while i < 32 and (target >> i & 1) == 0:
        i += 1
    if i == 32:
        break

We skip bit positions in target that are 0 since we don't need them.

Step 4: Combine lower bits if needed

while j < i:
    cnt[j + 1] += cnt[j] // 2
    cnt[j] %= 2
    j += 1

If j < i, we have bits at positions lower than what we need. We can combine pairs of bits at position j to form bits at position j+1. This is like combining two 1's to make a 2, or two 2's to make a 4. We add cnt[j] // 2 to the next position and keep the remainder at the current position.

Step 5: Find the nearest available bit

while cnt[j] == 0:
    cnt[j] = 1
    j += 1

If we don't have a bit at position j, we need to find the next available bit at a higher position. As we move up, we mark each position with 1 because when we split from position j down to position i, we'll create bits at all intermediate positions.

Step 6: Perform the split and count operations

ans += j - i
cnt[j] -= 1
j = i
i += 1

The number of operations needed is j - i (the distance we need to split down). We subtract 1 from cnt[j] since we used that bit, reset j to i, and move to the next bit position in target.

The algorithm continues until all required bits in target are satisfied. The key insight is that we process bits greedily from low to high, always using the nearest available bit and minimizing the number of split operations needed.

Ready to land your dream job?

Unlock your dream job with a 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through a concrete example to understand how the solution works.

Example: nums = [1, 4, 16], target = 9

First, let's understand what we need:

  • Target 9 in binary is 1001, which means we need bits at positions 0 and 3 (representing 1 and 8)
  • Our initial array has: 1 (bit at position 0), 4 (bit at position 2), 16 (bit at position 4)

Step 1: Initial Setup

  • Sum check: 1 + 4 + 16 = 21 β‰₯ 9 βœ“ (solution is possible)
  • Count bits at each position:
    • cnt[0] = 1 (from the number 1)
    • cnt[2] = 1 (from the number 4)
    • cnt[4] = 1 (from the number 16)
    • All other positions = 0

Step 2: Process bit position 0 (need 1)

  • We need bit at position 0 for target
  • We already have cnt[0] = 1
  • Use it directly: 0 operations needed
  • Update: cnt[0] = 0

Step 3: Process bit position 3 (need 8)

  • Skip positions 1 and 2 in target (they're 0)
  • We need bit at position 3 for target
  • Check cnt[3] = 0 (we don't have an 8)
  • Look for the next available bit: cnt[4] = 1 (we have a 16)
  • Split 16 β†’ two 8's: 1 operation needed
  • Use one 8 for our target
  • Update: cnt[4] = 0, cnt[3] = 1 (one 8 remains)

Final Result: 1 operation

Let's verify: After 1 operation, our array becomes [1, 4, 8, 8]. We can select 1 and 8 to sum to 9.


Another Example: nums = [1, 32], target = 12

Target 12 in binary is 1100, which means we need bits at positions 2 and 3 (representing 4 and 8).

Step 1: Initial Setup

  • Sum check: 1 + 32 = 33 β‰₯ 12 βœ“
  • Count bits: cnt[0] = 1, cnt[5] = 1

Step 2: Process bit position 2 (need 4)

  • Skip positions 0 and 1 in target
  • Need bit at position 2
  • Check positions: cnt[2] = 0, cnt[3] = 0, cnt[4] = 0, cnt[5] = 1
  • Split operations needed:
    • 32 β†’ two 16's (position 5 to 4): 1 operation
    • 16 β†’ two 8's (position 4 to 3): 1 operation
    • 8 β†’ two 4's (position 3 to 2): 1 operation
  • Total: 3 operations to get a 4
  • Update: cnt[5] = 0, cnt[2] = 1 (one 4 used, one remains)

Step 3: Process bit position 3 (need 8)

  • Need bit at position 3
  • From our previous splits, we have cnt[3] = 1 (one 8 remains from splitting)
  • Use it directly: 0 additional operations

Final Result: 3 operations

The algorithm efficiently tracks available bits and performs the minimum splits needed to form the target sum.

Solution Implementation

1class Solution:
2    def minOperations(self, nums: List[int], target: int) -> int:
3        # Check if the sum of all numbers is less than target (impossible case)
4        total_sum = sum(nums)
5        if total_sum < target:
6            return -1
7      
8        # Count frequency of each bit position (0 to 31)
9        # bit_count[i] represents how many numbers have bit i set
10        bit_count = [0] * 32
11        for num in nums:
12            for bit_pos in range(32):
13                if (num >> bit_pos) & 1:
14                    bit_count[bit_pos] += 1
15      
16        # Initialize pointers and operation counter
17        target_bit_pos = 0  # Current bit position in target we're trying to satisfy
18        current_bit_pos = 0  # Current bit position we're processing
19        operations = 0
20      
21        while True:
22            # Find the next bit position that is set in target
23            while target_bit_pos < 32 and ((target >> target_bit_pos) & 1) == 0:
24                target_bit_pos += 1
25          
26            # If we've processed all bits in target, we're done
27            if target_bit_pos == 32:
28                break
29          
30            # Carry forward excess bits from lower positions to higher positions
31            # Two bits at position i can combine to form one bit at position i+1
32            while current_bit_pos < target_bit_pos:
33                bit_count[current_bit_pos + 1] += bit_count[current_bit_pos] // 2
34                bit_count[current_bit_pos] %= 2
35                current_bit_pos += 1
36          
37            # Find the nearest available bit at or above current position
38            # Each split operation increases the position difference
39            while bit_count[current_bit_pos] == 0:
40                bit_count[current_bit_pos] = 1
41                current_bit_pos += 1
42          
43            # Add the number of split operations needed
44            # (difference between where we found the bit and where we need it)
45            operations += current_bit_pos - target_bit_pos
46          
47            # Use one bit from current position
48            bit_count[current_bit_pos] -= 1
49          
50            # Reset current position to target position for next iteration
51            current_bit_pos = target_bit_pos
52            target_bit_pos += 1
53      
54        return operations
55
1class Solution {
2    public int minOperations(List<Integer> nums, int target) {
3        // Calculate total sum and count bits at each position
4        long totalSum = 0;
5        int[] bitCount = new int[32];  // bitCount[i] represents count of numbers with bit i set
6      
7        for (int num : nums) {
8            totalSum += num;
9            // Count which bit positions are set in this number
10            for (int bitPos = 0; bitPos < 32; bitPos++) {
11                if ((num >> bitPos & 1) == 1) {
12                    bitCount[bitPos]++;
13                }
14            }
15        }
16      
17        // If total sum is less than target, impossible to achieve
18        if (totalSum < target) {
19            return -1;
20        }
21      
22        int targetBitIndex = 0;  // Current bit position in target we're trying to satisfy
23        int processBitIndex = 0;  // Current bit position we're processing in bitCount
24        int operations = 0;       // Total number of split operations needed
25      
26        while (true) {
27            // Find next set bit in target
28            while (targetBitIndex < 32 && (target >> targetBitIndex & 1) == 0) {
29                targetBitIndex++;
30            }
31          
32            // If no more set bits in target, we're done
33            if (targetBitIndex == 32) {
34                return operations;
35            }
36          
37            // Merge smaller bits up to current target bit position
38            // Two numbers of 2^i can be combined to form one 2^(i+1)
39            while (processBitIndex < targetBitIndex) {
40                bitCount[processBitIndex + 1] += bitCount[processBitIndex] / 2;
41                bitCount[processBitIndex] %= 2;
42                processBitIndex++;
43            }
44          
45            // Find the next available bit that can be split down
46            // If current position has no bits, we need to split from a higher position
47            while (bitCount[processBitIndex] == 0) {
48                bitCount[processBitIndex] = 1;  // Mark that we'll split into this position
49                processBitIndex++;
50            }
51          
52            // Calculate operations needed (difference between where we found a bit and where we need it)
53            operations += processBitIndex - targetBitIndex;
54          
55            // Use one bit from current position
56            bitCount[processBitIndex]--;
57          
58            // Reset process index to target index for next iteration
59            processBitIndex = targetBitIndex;
60            targetBitIndex++;
61        }
62    }
63}
64
1class Solution {
2public:
3    int minOperations(vector<int>& nums, int target) {
4        // Calculate total sum and count frequency of each bit position
5        long long totalSum = 0;
6        int bitCount[32] = {0};  // bitCount[i] stores count of numbers with bit i set
7      
8        for (int num : nums) {
9            totalSum += num;
10            // Count which bit positions are set in this number
11            for (int bitPos = 0; bitPos < 32; ++bitPos) {
12                if ((num >> bitPos) & 1) {
13                    ++bitCount[bitPos];
14                }
15            }
16        }
17      
18        // If total sum is less than target, impossible to form target
19        if (totalSum < target) {
20            return -1;
21        }
22      
23        int targetBitIndex = 0;  // Current bit position in target we're trying to satisfy
24        int processBitIndex = 0;  // Current bit position we're processing in our array
25        int operations = 0;       // Number of split operations performed
26      
27        while (true) {
28            // Find next set bit in target
29            while (targetBitIndex < 32 && ((target >> targetBitIndex) & 1) == 0) {
30                ++targetBitIndex;
31            }
32          
33            // If no more set bits in target, we're done
34            if (targetBitIndex == 32) {
35                return operations;
36            }
37          
38            // Carry forward: combine pairs of smaller powers to get larger powers
39            while (processBitIndex < targetBitIndex) {
40                bitCount[processBitIndex + 1] += bitCount[processBitIndex] / 2;
41                bitCount[processBitIndex] %= 2;
42                ++processBitIndex;
43            }
44          
45            // Find the next available bit position that has a count > 0
46            // This represents splitting larger powers if needed
47            while (bitCount[processBitIndex] == 0) {
48                bitCount[processBitIndex] = 1;  // Mark as used after splitting
49                ++processBitIndex;
50            }
51          
52            // Add the number of splits needed (difference in bit positions)
53            operations += processBitIndex - targetBitIndex;
54          
55            // Use one bit from current position
56            --bitCount[processBitIndex];
57          
58            // Reset process index to current target bit position
59            processBitIndex = targetBitIndex;
60            ++targetBitIndex;
61        }
62    }
63};
64
1/**
2 * Calculates the minimum number of operations to form a target value
3 * by combining elements from the nums array (powers of 2).
4 * Operations involve splitting a number into two halves.
5 * 
6 * @param nums - Array of numbers (powers of 2)
7 * @param target - Target value to achieve
8 * @returns Minimum operations needed, or -1 if impossible
9 */
10function minOperations(nums: number[], target: number): number {
11    // Calculate total sum and count bits at each position
12    let totalSum: number = 0;
13    const bitCount: number[] = Array(32).fill(0);
14  
15    // Process each number in the input array
16    for (const num of nums) {
17        totalSum += num;
18      
19        // Count the bit positions where this number has a 1
20        for (let bitPos = 0; bitPos < 32; bitPos++) {
21            if ((num >> bitPos) & 1) {
22                bitCount[bitPos]++;
23            }
24        }
25    }
26  
27    // If total sum is less than target, it's impossible
28    if (totalSum < target) {
29        return -1;
30    }
31  
32    let operations: number = 0;
33    let targetBitIndex: number = 0;  // Current bit position in target
34    let processBitIndex: number = 0; // Current bit position being processed
35  
36    // Main loop to process each required bit in the target
37    while (true) {
38        // Find the next set bit in the target
39        while (targetBitIndex < 32 && ((target >> targetBitIndex) & 1) === 0) {
40            targetBitIndex++;
41        }
42      
43        // If we've processed all bits, return the result
44        if (targetBitIndex === 32) {
45            return operations;
46        }
47      
48        // Consolidate lower bits by carrying forward
49        while (processBitIndex < targetBitIndex) {
50            bitCount[processBitIndex + 1] += bitCount[processBitIndex] >> 1;
51            bitCount[processBitIndex] %= 2;
52            processBitIndex++;
53        }
54      
55        // Find the nearest available bit by splitting higher bits if needed
56        while (bitCount[processBitIndex] === 0) {
57            bitCount[processBitIndex] = 1;
58            processBitIndex++;
59        }
60      
61        // Add the cost of splitting operations
62        operations += processBitIndex - targetBitIndex;
63      
64        // Use one bit from the current position
65        bitCount[processBitIndex]--;
66      
67        // Reset process index and move to next target bit
68        processBitIndex = targetBitIndex;
69        targetBitIndex++;
70    }
71}
72

Time and Space Complexity

Time Complexity: O(n Γ— log M + log target)

The time complexity consists of two main parts:

  • Building the count array: The first loop iterates through all n elements in nums. For each element x, it checks up to 32 bits (or log M bits where M is the maximum value), resulting in O(n Γ— log M) time.
  • Processing the target: The while loops process each bit position of the target. In the worst case, this involves iterating through all 32 bit positions with potential carry operations, contributing O(log target) time.

Since log target is typically bounded by 32 (for 32-bit integers) and is often smaller than n Γ— log M, the overall time complexity is O(n Γ— log M).

Space Complexity: O(log M)

The space complexity is determined by:

  • The cnt array which has a fixed size of 32 elements, representing bit positions up to 2^31. This requires O(32) = O(log M) space, where M is the maximum possible value that can be represented.
  • A few integer variables (i, j, ans, s) which require O(1) space.

Therefore, the overall space complexity is O(log M).

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Incorrect Bit Carrying Logic

A common mistake is not properly handling the carry-forward of excess bits from lower positions. When we have multiple bits at the same position, we can combine pairs to form bits at higher positions.

Incorrect approach:

# Wrong: Only moving to the next position without carrying
while current_bit_pos < target_bit_pos:
    current_bit_pos += 1

Correct approach:

# Correct: Carry pairs of bits to higher positions
while current_bit_pos < target_bit_pos:
    bit_count[current_bit_pos + 1] += bit_count[current_bit_pos] // 2
    bit_count[current_bit_pos] %= 2
    current_bit_pos += 1

2. Not Marking Intermediate Positions During Split Search

When searching for an available bit at a higher position, forgetting to mark intermediate positions with 1 leads to incorrect operation counting. When we split a higher bit down to a lower position, we create bits at all intermediate positions.

Incorrect approach:

# Wrong: Just finding the position without marking intermediate bits
while bit_count[current_bit_pos] == 0:
    current_bit_pos += 1

Correct approach:

# Correct: Mark each intermediate position with 1
while bit_count[current_bit_pos] == 0:
    bit_count[current_bit_pos] = 1  # This is crucial!
    current_bit_pos += 1

3. Overflow with Large Numbers

Using 31 or fewer bits might cause issues with the maximum possible values. Always use 32 bits to handle numbers up to 2^31.

Incorrect approach:

bit_count = [0] * 30  # Not enough bits

Correct approach:

bit_count = [0] * 32  # Sufficient for all valid inputs

4. Not Resetting the Current Position Pointer

After using a bit, failing to reset current_bit_pos to target_bit_pos causes the algorithm to skip necessary carry operations in subsequent iterations.

Incorrect approach:

operations += current_bit_pos - target_bit_pos
bit_count[current_bit_pos] -= 1
# Forgot to reset current_bit_pos
target_bit_pos += 1

Correct approach:

operations += current_bit_pos - target_bit_pos
bit_count[current_bit_pos] -= 1
current_bit_pos = target_bit_pos  # Reset to process next target bit
target_bit_pos += 1

5. Misunderstanding the Problem Constraints

Some might think we need to use ALL elements to form the target, but we only need to select a subsequence. The check should only ensure the total sum is at least the target, not exactly the target.

Incorrect approach:

if sum(nums) != target:  # Wrong: We don't need exact sum
    return -1

Correct approach:

if sum(nums) < target:  # Correct: Just need enough sum available
    return -1
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

What is an advantages of top-down dynamic programming vs bottom-up dynamic programming?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More