2835. Minimum Operations to Form Subsequence With Target Sum
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.
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
= binary0001
- 2 =
2^1
= binary0010
- 4 =
2^2
= binary0100
- 8 =
2^3
= binary1000
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 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 20 bill.
The algorithm therefore:
- Counts how many bits we have at each position across all numbers
- For each bit needed in
target
(from low to high), finds the nearest available bit - Splits down if necessary, keeping track of the operations needed
- 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 intarget
j
points to the position we're currently processing incnt
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 EvaluatorExample 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 innums
. For each elementx
, it checks up to 32 bits (orlog M
bits whereM
is the maximum value), resulting inO(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 to2^31
. This requiresO(32)
=O(log M)
space, whereM
is the maximum possible value that can be represented. - A few integer variables (
i
,j
,ans
,s
) which requireO(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
What is an advantages of top-down dynamic programming vs bottom-up dynamic programming?
Recommended Readings
Greedy Introduction div class responsive iframe iframe src https www youtube com embed WTslqPbj7I title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture web share allowfullscreen iframe div When do we use greedy Greedy algorithms tend to solve optimization problems Typically they will ask you to calculate the max min of some value Commonly you may see this phrased in the problem as max min longest shortest largest smallest etc These keywords can be identified by just scanning
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Donβt Miss This!