Facebook Pixel

1558. Minimum Numbers of Function Calls to Make Target Array

MediumGreedyBit ManipulationArray
Leetcode Link

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:

  1. Add 1 to a single element: Choose any index i and increment arr[i] by 1
  2. 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)
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. Each 1-bit in a number requires exactly one "add 1" operation
  2. 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 array
  • bit_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 need k doubling operations (not k+1)
    • Example: 4 = 100 has bit_length = 3, but needs only 2 doublings (0β†’0, add 1β†’1, doubleβ†’2, doubleβ†’4)
  • 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 Evaluator

Example 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 all n elements in the array
  • For each element v, bit_count() counts the number of set bits, which takes O(log(v)) time in the worst case (where v is the value of the element)
  • The max(nums) operation takes O(n) time to find the maximum element
  • The bit_length() operation on the maximum element takes O(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.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which algorithm is best for finding the shortest distance between two points in an unweighted graph?


Recommended Readings

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

Load More