1558. Minimum Numbers of Function Calls to Make Target Array
Problem Description
You start with an array arr
of the same length as nums
, where all values in arr
are initially set to 0. You need to transform arr
into nums
using a special modify
function.
The modify
function allows you to perform one of two operations on arr
:
- Add 1 to a single element: Choose any index
i
and incrementarr[i]
by 1 - Double all elements: Multiply every element in
arr
by 2
Your goal is to find the minimum number of function calls needed to transform the initial array arr
(all zeros) into the target array nums
.
For example, if nums = [3]
:
- Start with
arr = [0]
- Call 1: Add 1 to index 0 β
arr = [1]
- Call 2: Double all elements β
arr = [2]
- Call 3: Add 1 to index 0 β
arr = [3]
- Total: 3 function calls
The solution leverages bit manipulation insights:
- Each "add 1" operation corresponds to setting a bit in the binary representation
- Each "double all" operation corresponds to a left shift of all numbers
- The minimum operations equals the sum of all 1-bits in all numbers (for individual additions) plus the maximum number of doubling operations needed (determined by the position of the highest bit among all numbers minus 1)
Intuition
To understand how to minimize operations, let's think about how we build each number from 0. Consider building the number 5 (binary: 101
):
- We need to set the bits at positions 0 and 2
- We can either add 1 five times, or be smarter about it
The key insight comes from examining the operations in reverse - how would we reduce each number in nums
back to 0?
- Adding 1 becomes subtracting 1
- Doubling all elements becomes halving all elements (when all are even)
Working backwards from nums = [5]
(binary 101
):
5
β4
(subtract 1, removes the rightmost 1-bit)4
β2
(divide by 2, right shift)2
β1
(divide by 2, right shift)1
β0
(subtract 1, removes the last 1-bit)
This reveals that:
- Each 1-bit in a number requires exactly one "add 1" operation
- The position of the highest 1-bit determines how many doubling operations we need
Since doubling affects all elements simultaneously, we only need enough doubling operations to accommodate the largest number. The number of doublings equals the position of the highest bit minus 1 (since we start counting positions from 0).
For nums = [3, 2, 2, 4]
:
- Binary representations:
[11, 10, 10, 100]
- Total 1-bits: 2 + 1 + 1 + 1 = 5 (individual add operations)
- Highest bit position: 2 (in the number 4 =
100
) - Doubling operations needed: 2
Total operations = 5 + 2 = 7
This approach works because we can strategically choose when to add 1 to specific elements between the doubling operations, effectively "building up" each number's binary representation bit by bit.
Learn more about Greedy patterns.
Solution Approach
The solution implements the bit manipulation insight discovered in our intuition using Python's built-in bit operations:
class Solution:
def minOperations(self, nums: List[int]) -> int:
return sum(v.bit_count() for v in nums) + max(0, max(nums).bit_length() - 1)
Let's break down the implementation:
1. Counting Individual Add Operations:
sum(v.bit_count() for v in nums)
bit_count()
returns the number of 1-bits in the binary representation of a number- For each number in
nums
, we count its 1-bits - The sum gives us the total number of "add 1" operations needed across all elements
2. Counting Doubling Operations:
max(0, max(nums).bit_length() - 1)
max(nums)
finds the largest number in the arraybit_length()
returns the number of bits needed to represent the number (position of highest bit + 1)- We subtract 1 because:
- If the highest bit is at position
k
, we needk
doubling operations (notk+1
) - Example: 4 =
100
has bit_length = 3, but needs only 2 doublings (0β0, add 1β1, doubleβ2, doubleβ4)
- If the highest bit is at position
max(0, ...)
handles the edge case when the array contains only zeros
Example Walkthrough:
For nums = [1, 5]
:
- Binary:
[1, 101]
- Individual adds:
1.bit_count() + 5.bit_count() = 1 + 2 = 3
- Maximum number: 5 with
bit_length() = 3
- Doubling operations:
3 - 1 = 2
- Total:
3 + 2 = 5
The algorithm runs in O(n)
time where n
is the length of the array, as we iterate through the array once to count bits and find the maximum. The space complexity is O(1)
as we only use a constant amount of extra space.
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 trace through the solution with nums = [2, 3]
to see how the bit manipulation approach works.
Step 1: Convert to binary and analyze
2
in binary:10
(1 bit set)3
in binary:11
(2 bits set)
Step 2: Count individual "add 1" operations
- For
2
:bit_count() = 1
(one 1-bit) - For
3
:bit_count() = 2
(two 1-bits) - Total add operations:
1 + 2 = 3
Step 3: Determine doubling operations needed
- Maximum number:
3
3.bit_length() = 2
(needs 2 bits to represent)- Doubling operations:
2 - 1 = 1
Step 4: Calculate total
- Total operations:
3 + 1 = 4
Verification by building the array:
- Start:
arr = [0, 0]
- Call 1: Add 1 to index 0 β
arr = [1, 0]
- Call 2: Add 1 to index 1 β
arr = [1, 1]
- Call 3: Double all β
arr = [2, 2]
- Call 4: Add 1 to index 1 β
arr = [2, 3]
We successfully transformed [0, 0]
to [2, 3]
in exactly 4 operations, matching our calculation. The key insight is that we perform all necessary doublings (determined by the largest number) and strategically place our "add 1" operations to set the required bits in each number's binary representation.
Solution Implementation
1class Solution:
2 def minOperations(self, nums: List[int]) -> int:
3 """
4 Calculate minimum operations to reduce all numbers to zero.
5
6 The algorithm counts:
7 1. Total number of 1-bits across all numbers (subtraction operations)
8 2. Maximum bit position minus 1 (division by 2 operations)
9
10 Args:
11 nums: List of non-negative integers
12
13 Returns:
14 Minimum number of operations required
15 """
16 # Count total number of set bits (1s) in binary representation of all numbers
17 # This represents the number of subtraction operations needed
18 total_set_bits = sum(num.bit_count() for num in nums)
19
20 # Find the highest bit position among all numbers
21 # bit_length() - 1 gives the number of division operations needed
22 # max(0, ...) ensures we don't return negative for empty array or all zeros
23 max_bit_position = max(0, max(nums).bit_length() - 1) if nums else 0
24
25 # Total operations = subtraction operations + division operations
26 return total_set_bits + max_bit_position
27
1class Solution {
2 public int minOperations(int[] nums) {
3 // Total operations counter
4 int totalOperations = 0;
5
6 // Track the maximum value to determine division operations needed
7 int maxValue = 0;
8
9 // Iterate through all numbers in the array
10 for (int number : nums) {
11 // Update maximum value encountered
12 maxValue = Math.max(maxValue, number);
13
14 // Add count of 1-bits (represents subtraction operations needed)
15 // Each 1-bit in binary representation requires a subtraction operation
16 totalOperations += Integer.bitCount(number);
17 }
18
19 // Calculate number of division-by-2 operations needed
20 // This equals the bit length of max value minus 1
21 // (or the position of the highest set bit)
22 int divisionOperations = Integer.toBinaryString(maxValue).length() - 1;
23 totalOperations += divisionOperations;
24
25 return totalOperations;
26 }
27}
28
1class Solution {
2public:
3 int minOperations(vector<int>& nums) {
4 int totalOperations = 0;
5 int maxValue = 0;
6
7 // Iterate through all numbers in the array
8 for (int currentValue : nums) {
9 // Track the maximum value in the array
10 maxValue = max(maxValue, currentValue);
11
12 // Count the number of 1-bits in current value
13 // This represents the number of subtract-1 operations needed
14 totalOperations += __builtin_popcount(currentValue);
15 }
16
17 // If there's at least one non-zero element in the array
18 if (maxValue > 0) {
19 // Calculate the number of divide-by-2 operations needed
20 // This is determined by the position of the highest bit in the maximum value
21 // __builtin_clz counts leading zeros in a 32-bit integer
22 // 31 - __builtin_clz(maxValue) gives the 0-based index of the highest set bit
23 totalOperations += 31 - __builtin_clz(maxValue);
24 }
25
26 return totalOperations;
27 }
28};
29
1function minOperations(nums: number[]): number {
2 let totalOperations = 0;
3 let maxValue = 0;
4
5 // Iterate through all numbers in the array
6 for (const currentValue of nums) {
7 // Track the maximum value in the array
8 maxValue = Math.max(maxValue, currentValue);
9
10 // Count the number of 1-bits in current value
11 // This represents the number of subtract-1 operations needed
12 totalOperations += countOneBits(currentValue);
13 }
14
15 // If there's at least one non-zero element in the array
16 if (maxValue > 0) {
17 // Calculate the number of divide-by-2 operations needed
18 // This is determined by the position of the highest bit in the maximum value
19 // We need to find the 0-based index of the highest set bit
20 totalOperations += getHighestBitPosition(maxValue);
21 }
22
23 return totalOperations;
24}
25
26// Helper function to count the number of 1-bits in a number
27function countOneBits(num: number): number {
28 let count = 0;
29 while (num > 0) {
30 count += num & 1;
31 num >>>= 1; // Unsigned right shift
32 }
33 return count;
34}
35
36// Helper function to get the position of the highest set bit (0-based index)
37function getHighestBitPosition(num: number): number {
38 if (num === 0) return -1;
39
40 let position = 0;
41 while (num > 1) {
42 num >>>= 1; // Unsigned right shift
43 position++;
44 }
45 return position;
46}
47
Time and Space Complexity
Time Complexity: O(n * log(max_value))
The time complexity breaks down as follows:
- The
sum(v.bit_count() for v in nums)
iterates through alln
elements in the array - For each element
v
,bit_count()
counts the number of set bits, which takesO(log(v))
time in the worst case (wherev
is the value of the element) - The
max(nums)
operation takesO(n)
time to find the maximum element - The
bit_length()
operation on the maximum element takesO(log(max_value))
time - Overall:
O(n * log(max_value)) + O(n) + O(log(max_value))
=O(n * log(max_value))
Where max_value
represents the maximum value in the input array.
Space Complexity: O(1)
The space complexity analysis:
- The generator expression
v.bit_count() for v in nums
doesn't create an intermediate list, it computes values on-the-fly - Only a constant amount of extra space is used for variables to store the sum and maximum value
- No additional data structures proportional to the input size are created
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Misunderstanding the Operation Direction
A critical pitfall is misinterpreting the problem direction. The problem states we're transforming from zeros to the target array, but many developers mistakenly think about it in reverse (reducing the target to zeros). This leads to confusion about when to use division vs. multiplication.
Wrong Mental Model:
# Thinking backwards - reducing nums to zeros
def minOperations(self, nums: List[int]) -> int:
operations = 0
while any(num > 0 for num in nums):
# Divide when possible (wrong direction!)
if all(num % 2 == 0 for num in nums):
nums = [num // 2 for num in nums]
else:
# Subtract from odd numbers
for i in range(len(nums)):
if nums[i] % 2 == 1:
nums[i] -= 1
operations += 1
operations += 1
return operations
Correct Approach: Think of building UP from zeros to the target, not reducing DOWN. The bit manipulation insight becomes clearer: we're adding bits (add operations) and shifting left (doubling operations).
2. Off-by-One Error with Bit Length
Forgetting to subtract 1 from bit_length()
is a common mistake. The bit_length()
function returns the number of bits needed to represent a number, but the number of doubling operations is one less than this.
Incorrect:
def minOperations(self, nums: List[int]) -> int:
# Wrong: uses bit_length() directly without subtracting 1
return sum(v.bit_count() for v in nums) + max(nums).bit_length()
Why it's wrong: For number 4 (binary: 100), bit_length()
returns 3, but we only need 2 doublings:
- Start: 0
- Add 1: 1
- Double: 2
- Double: 4 (only 2 doublings, not 3!)
3. Not Handling Edge Cases
Failing to handle empty arrays or arrays containing only zeros can cause runtime errors.
Problematic Code:
def minOperations(self, nums: List[int]) -> int:
# This crashes on empty array!
return sum(v.bit_count() for v in nums) + max(nums).bit_length() - 1
Robust Solution:
def minOperations(self, nums: List[int]) -> int:
if not nums or max(nums) == 0:
return 0
return sum(v.bit_count() for v in nums) + max(nums).bit_length() - 1
Or more concisely using max(0, ...)
:
def minOperations(self, nums: List[int]) -> int:
return sum(v.bit_count() for v in nums) + max(0, max(nums, default=0).bit_length() - 1)
4. Inefficient Simulation Approach
Some developers try to simulate the actual transformation process, which is inefficient and unnecessary.
Inefficient Simulation:
def minOperations(self, nums: List[int]) -> int:
arr = [0] * len(nums)
operations = 0
while arr != nums:
# Check if we should double
should_double = False
for i in range(len(nums)):
if arr[i] * 2 <= nums[i]:
should_double = True
break
if should_double and all(arr[i] > 0 or nums[i] == 0 for i in range(len(arr))):
arr = [x * 2 for x in arr]
else:
# Add 1 to an element that needs it
for i in range(len(nums)):
if arr[i] < nums[i]:
arr[i] += 1
break
operations += 1
return operations
This approach has O(max(nums) * n) time complexity and is much harder to implement correctly. The bit manipulation solution is both more elegant and efficient with O(n) complexity.
Which algorithm is best for finding the shortest distance between two points in an unweighted graph?
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!