Facebook Pixel

3326. Minimum Division Operations to Make Array Non Decreasing

MediumGreedyArrayMathNumber Theory
Leetcode Link

Problem Description

You are given an integer array nums.

A proper divisor of a natural number x is any positive divisor that is strictly less than x. For example, 2 is a proper divisor of 4 (since 2 divides 4 and 2 < 4), while 6 is not a proper divisor of 6 (since 6 is not less than 6).

You can perform operations on the array where each operation consists of:

  1. Select any one element from nums
  2. Divide it by its greatest proper divisor

Your goal is to make the array non-decreasing (each element is less than or equal to the next element) using the minimum number of operations.

Return the minimum number of operations needed to achieve this. If it's impossible to make the array non-decreasing, return -1.

Key insights about the operation:

  • When you divide a number by its greatest proper divisor, you're essentially finding the smallest value that number can become through this operation
  • For a prime number p, the greatest proper divisor is 1, so p / 1 = p (no change)
  • For a composite number n, dividing by its greatest proper divisor gives you the smallest prime factor of n

Example scenarios:

  • If a number is 12, its proper divisors are {1, 2, 3, 4, 6}. The greatest is 6, so 12 / 6 = 2
  • If a number is 7 (prime), its only proper divisor is 1, so 7 / 1 = 7 (unchanged)

The strategy involves checking from right to left: if an element is greater than the next element, try to reduce it using the operation. If even after reduction it's still greater than the next element, the task is impossible.

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

Intuition

The key observation is understanding what happens when we divide a number by its greatest proper divisor.

For any composite number n, if we divide it by its greatest proper divisor, we get the smallest prime factor of n. Why? Because if n = p × q where p is the smallest prime factor, then q must be the greatest proper divisor. So n / q = p.

For a prime number, the only proper divisor is 1, so dividing by 1 doesn't change the number. This means once a number becomes prime, it cannot be reduced further.

Now, when should we apply operations? We want a non-decreasing array with minimum operations. A greedy approach makes sense: only change a number when absolutely necessary.

Working from right to left is crucial here. Why? Because:

  • The rightmost element sets a constraint for elements to its left
  • If we need to reduce an element, we want to know the maximum value it needs to be less than or equal to
  • By processing right to left, we always know what constraint the current element must satisfy

When we encounter nums[i] > nums[i+1], we must reduce nums[i]. The operation converts it to its smallest prime factor (if it's composite) or keeps it unchanged (if it's prime). This is the only possible reduction for that number.

If after this single reduction nums[i] is still greater than nums[i+1], we know it's impossible to fix the array. Why? Because we've already applied the maximum possible reduction to that element - it's now either unchanged (was prime) or has become its smallest prime factor (was composite), and we can't reduce it any further.

This leads to preprocessing: we compute the smallest prime factor for all possible numbers using a sieve-like approach. Then we traverse right to left, applying the reduction only when needed, counting operations along the way.

Learn more about Greedy and Math patterns.

Solution Approach

The solution consists of two main parts: preprocessing to find the smallest prime factor for each number, and then applying a greedy algorithm from right to left.

Step 1: Preprocessing - Finding Smallest Prime Factors

We create an array lpf (least prime factor) where lpf[i] stores the smallest prime factor of i. The preprocessing uses a sieve-like approach:

mx = 10**6 + 1
lpf = [0] * (mx + 1)
for i in range(2, mx + 1):
    if lpf[i] == 0:  # i is prime
        for j in range(i, mx + 1, i):
            if lpf[j] == 0:  # j hasn't been assigned a prime factor yet
                lpf[j] = i
  • We iterate through numbers from 2 to mx
  • If lpf[i] == 0, then i is prime (hasn't been marked by any smaller prime)
  • For each prime i, we mark all its multiples j with lpf[j] = i (but only if j hasn't been marked yet, ensuring we get the smallest prime factor)

Step 2: Greedy Algorithm from Right to Left

Once we have the smallest prime factors, we traverse the array from right to left:

def minOperations(self, nums: List[int]) -> int:
    ans = 0
    for i in range(len(nums) - 2, -1, -1):
        if nums[i] > nums[i + 1]:
            nums[i] = lpf[nums[i]]  # Replace with smallest prime factor
            if nums[i] > nums[i + 1]:  # Still greater? Impossible!
                return -1
            ans += 1
    return ans

The algorithm works as follows:

  1. Start from the second-to-last element and move leftward
  2. At each position i, check if nums[i] > nums[i + 1]
  3. If yes, we must reduce nums[i] by replacing it with its smallest prime factor: nums[i] = lpf[nums[i]]
  4. After reduction, check again if nums[i] > nums[i + 1]
    • If still greater, return -1 (impossible to make non-decreasing)
    • Otherwise, increment the operation count
  5. Continue until all elements are processed

Why This Works:

  • When we divide a number by its greatest proper divisor, we get its smallest prime factor
  • Each element can only be reduced once (to its smallest prime factor if composite, or stays same if prime)
  • By processing right to left, we ensure we make the minimum necessary changes
  • If an element can't be made small enough even after reduction, the task is impossible

Time Complexity:

  • Preprocessing: O(n log log n) for the sieve
  • Main algorithm: O(m) where m is the length of the input array
  • Overall: O(n log log n + m) where n is the maximum possible value in the array

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 the solution with nums = [25, 7, 14, 10].

Step 1: Precompute smallest prime factors

  • For 25: 25 = 5 × 5, so lpf[25] = 5
  • For 7: 7 is prime, so lpf[7] = 7
  • For 14: 14 = 2 × 7, so lpf[14] = 2
  • For 10: 10 = 2 × 5, so lpf[10] = 2

Step 2: Process array from right to left

Starting position: [25, 7, 14, 10], operations = 0

i = 2 (comparing 14 and 10):

  • Is 14 > 10? Yes, we need to reduce 14
  • Apply operation: 14 → lpf[14] = 2
  • Array becomes: [25, 7, 2, 10]
  • Is 2 > 10? No, we're good
  • operations = 1

i = 1 (comparing 7 and 2):

  • Is 7 > 2? Yes, we need to reduce 7
  • Apply operation: 7 → lpf[7] = 7 (stays the same, it's prime!)
  • Array remains: [25, 7, 2, 10]
  • Is 7 still > 2? Yes! This means it's impossible
  • Return -1

The array cannot be made non-decreasing because 7 is prime and cannot be reduced further, yet it needs to be ≤ 2.


Let's try another example where it's possible: nums = [15, 20, 10]

Step 1: Precompute smallest prime factors

  • lpf[15] = 3 (15 = 3 × 5)
  • lpf[20] = 2 (20 = 2 × 10)
  • lpf[10] = 2 (10 = 2 × 5)

Step 2: Process array from right to left

Starting position: [15, 20, 10], operations = 0

i = 1 (comparing 20 and 10):

  • Is 20 > 10? Yes, we need to reduce 20
  • Apply operation: 20 → lpf[20] = 2
  • Array becomes: [15, 2, 10]
  • Is 2 > 10? No, we're good
  • operations = 1

i = 0 (comparing 15 and 2):

  • Is 15 > 2? Yes, we need to reduce 15
  • Apply operation: 15 → lpf[15] = 3
  • Array becomes: [3, 2, 10]
  • Is 3 > 2? Yes! This is still greater
  • Return -1

Even this array cannot be made non-decreasing!


Final example where it works: nums = [4, 3, 10]

Step 1: Precompute smallest prime factors

  • lpf[4] = 2 (4 = 2 × 2)
  • lpf[3] = 3 (3 is prime)
  • lpf[10] = 2 (10 = 2 × 5)

Step 2: Process array from right to left

Starting position: [4, 3, 10], operations = 0

i = 1 (comparing 3 and 10):

  • Is 3 > 10? No, already in order
  • No operation needed

i = 0 (comparing 4 and 3):

  • Is 4 > 3? Yes, we need to reduce 4
  • Apply operation: 4 → lpf[4] = 2
  • Array becomes: [2, 3, 10]
  • Is 2 > 3? No, we're good
  • operations = 1

Final array: [2, 3, 10] (non-decreasing) Return: 1 operation

Solution Implementation

1from typing import List
2
3# Precompute smallest prime factors using Sieve of Eratosthenes
4MAX_VALUE = 10**6 + 1
5smallest_prime_factor = [0] * (MAX_VALUE + 1)
6
7for i in range(2, MAX_VALUE + 1):
8    if smallest_prime_factor[i] == 0:  # i is prime
9        # Mark all multiples of i with i as their smallest prime factor
10        for j in range(i, MAX_VALUE + 1, i):
11            if smallest_prime_factor[j] == 0:
12                smallest_prime_factor[j] = i
13
14
15class Solution:
16    def minOperations(self, nums: List[int]) -> int:
17        """
18        Finds minimum operations to make array non-decreasing.
19        Each operation replaces a number with its smallest prime factor.
20      
21        Args:
22            nums: List of integers to process
23          
24        Returns:
25            Minimum number of operations needed, or -1 if impossible
26        """
27        operation_count = 0
28      
29        # Process array from right to left
30        for i in range(len(nums) - 2, -1, -1):
31            # If current element is greater than next element, needs fixing
32            if nums[i] > nums[i + 1]:
33                # Replace with smallest prime factor
34                nums[i] = smallest_prime_factor[nums[i]]
35              
36                # Check if array is still not non-decreasing after replacement
37                if nums[i] > nums[i + 1]:
38                    return -1  # Impossible to make non-decreasing
39                  
40                operation_count += 1
41              
42        return operation_count
43
1class Solution {
2    // Maximum value constraint for the problem
3    private static final int MAX_VALUE = (int) 1e6 + 1;
4  
5    // Array to store the Least Prime Factor (LPF) for each number
6    // LPF[i] represents the smallest prime factor of number i
7    private static final int[] LPF = new int[MAX_VALUE + 1];
8  
9    // Static block to precompute the Least Prime Factor for all numbers up to MAX_VALUE
10    static {
11        // Use Sieve-like approach to find the least prime factor
12        for (int i = 2; i <= MAX_VALUE; ++i) {
13            // Mark i as the least prime factor for all its multiples
14            for (int j = i; j <= MAX_VALUE; j += i) {
15                // Only set LPF if it hasn't been set before (ensuring it's the smallest)
16                if (LPF[j] == 0) {
17                    LPF[j] = i;
18                }
19            }
20        }
21    }
22  
23    /**
24     * Finds the minimum number of operations to make the array non-increasing
25     * by replacing elements with their least prime factor.
26     * 
27     * @param nums the input array to process
28     * @return minimum number of operations needed, or -1 if impossible
29     */
30    public int minOperations(int[] nums) {
31        int operationCount = 0;
32      
33        // Traverse the array from right to left (second-to-last element to first)
34        for (int i = nums.length - 2; i >= 0; i--) {
35            // Check if current element violates non-increasing order
36            if (nums[i] > nums[i + 1]) {
37                // Replace current element with its least prime factor
38                nums[i] = LPF[nums[i]];
39              
40                // Check if the array is still not non-increasing after replacement
41                if (nums[i] > nums[i + 1]) {
42                    // Impossible to make array non-increasing
43                    return -1;
44                }
45              
46                // Increment operation counter
47                operationCount++;
48            }
49        }
50      
51        return operationCount;
52    }
53}
54
1// Maximum value for array elements
2const int MAX_VALUE = 1e6;
3
4// Array to store the Least Prime Factor (LPF) for each number
5// LPF[i] represents the smallest prime factor of number i
6int leastPrimeFactor[MAX_VALUE + 1];
7
8// Initialize the LPF array using Sieve of Eratosthenes approach
9// This lambda function runs automatically before main()
10auto initializeLPF = [] {
11    // Iterate through all numbers from 2 to MAX_VALUE
12    for (int i = 2; i <= MAX_VALUE; i++) {
13        // If LPF[i] is 0, then i is a prime number
14        if (leastPrimeFactor[i] == 0) {
15            // Mark all multiples of i with i as their least prime factor
16            for (int j = i; j <= MAX_VALUE; j += i) {
17                // Only set if not already set (to maintain "least" prime factor)
18                if (leastPrimeFactor[j] == 0) {
19                    leastPrimeFactor[j] = i;
20                }
21            }
22        }
23    }
24    return 0;
25}();
26
27class Solution {
28public:
29    /**
30     * Finds minimum number of operations to make array non-increasing
31     * Each operation replaces a number with its least prime factor
32     * 
33     * @param nums Input array to be modified
34     * @return Minimum operations needed, or -1 if impossible
35     */
36    int minOperations(vector<int>& nums) {
37        int operationCount = 0;
38      
39        // Traverse array from second-to-last element to first
40        // Process from right to left to ensure valid comparisons
41        for (int i = nums.size() - 2; i >= 0; i--) {
42            // Check if current element violates non-increasing property
43            if (nums[i] > nums[i + 1]) {
44                // Replace current element with its least prime factor
45                nums[i] = leastPrimeFactor[nums[i]];
46              
47                // If still greater after replacement, array cannot be fixed
48                if (nums[i] > nums[i + 1]) {
49                    return -1;
50                }
51              
52                // Increment operation counter
53                operationCount++;
54            }
55        }
56      
57        return operationCount;
58    }
59};
60
1// Maximum value for preprocessing
2const MAX_VALUE = 10 ** 6;
3
4// Array to store the Least Prime Factor (LPF) for each number
5// lpf[i] represents the smallest prime factor of number i
6const leastPrimeFactor = Array(MAX_VALUE + 1).fill(0);
7
8// Precompute the least prime factor for all numbers up to MAX_VALUE
9// Using a sieve-like approach
10for (let i = 2; i <= MAX_VALUE; ++i) {
11    // For each number i, mark it as the least prime factor for all its multiples
12    for (let j = i; j <= MAX_VALUE; j += i) {
13        // Only set if no prime factor has been found yet
14        if (leastPrimeFactor[j] === 0) {
15            leastPrimeFactor[j] = i;
16        }
17    }
18}
19
20/**
21 * Finds the minimum number of operations to make the array non-increasing
22 * by replacing elements with their least prime factor
23 * 
24 * @param nums - The input array of positive integers
25 * @returns The minimum number of operations needed, or -1 if impossible
26 */
27function minOperations(nums: number[]): number {
28    let operationCount = 0;
29  
30    // Iterate from the second-to-last element to the first element
31    // Using bitwise NOT (~i) to check if i >= 0
32    for (let i = nums.length - 2; ~i; --i) {
33        // Check if current element is greater than the next element (violates non-increasing property)
34        if (nums[i] > nums[i + 1]) {
35            // Replace current element with its least prime factor
36            nums[i] = leastPrimeFactor[nums[i]];
37          
38            // After replacement, check if the array is still not non-increasing
39            if (nums[i] > nums[i + 1]) {
40                // If still greater, it's impossible to make the array non-increasing
41                return -1;
42            }
43          
44            // Increment the operation count
45            ++operationCount;
46        }
47    }
48  
49    return operationCount;
50}
51

Time and Space Complexity

The code consists of two parts: preprocessing to compute the least prime factor (LPF) array and the main solution.

Preprocessing Time Complexity: O(M × log log M), where M = 10^6. This uses a modified Sieve of Eratosthenes approach. For each prime number i, we iterate through its multiples up to M. The total number of operations is approximately M × (1/2 + 1/3 + 1/5 + 1/7 + ... ) for all primes up to M, which equals O(M × log log M).

Main Solution Time Complexity: O(n), where n is the length of the input array nums. The algorithm performs a single backward traversal through the array, with each element being processed in constant time (looking up lpf[nums[i]] is O(1)).

Overall Time Complexity: O(M × log log M + n), dominated by the preprocessing step since M is a large constant.

Space Complexity: O(M), where M = 10^6 + 1. The space is primarily used for the lpf array which stores the least prime factor for each number from 0 to M. The additional space used by the solution method is negligible compared to this.

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

Common Pitfalls

1. Incorrect Understanding of the Operation

Pitfall: Many developers mistakenly think they can repeatedly apply the operation to the same element multiple times to reduce it further. For example, thinking that 12 → 2 → 2 (applying operation twice).

Why it's wrong: Each element can only be operated on once. When you divide a number by its greatest proper divisor, you get its smallest prime factor, which is either prime itself or stays unchanged if already prime. You cannot reduce it further.

Solution: Remember that each element has at most one transformation:

  • Composite number → its smallest prime factor (one operation)
  • Prime number → stays the same (operation has no effect)

2. Processing Array from Left to Right

Pitfall: Attempting to process the array from left to right, trying to increase smaller elements to match larger ones.

Why it's wrong: The operation can only reduce numbers (or keep them the same for primes), never increase them. Processing left to right would lead to incorrect decisions about which elements need modification.

Solution: Always process from right to left. This ensures that when you reach position i, you already know the final value of nums[i+1], allowing you to make the correct decision about whether to reduce nums[i].

3. Memory Limit Issues with Preprocessing

Pitfall: Creating the smallest_prime_factor array with a fixed size of 10^6 + 1 can cause memory issues or be inefficient when the actual numbers in the input are much smaller.

Solution: Optimize the preprocessing based on the actual maximum value in the input:

def minOperations(self, nums: List[int]) -> int:
    max_val = max(nums)
  
    # Only compute up to what we need
    spf = [0] * (max_val + 1)
    for i in range(2, max_val + 1):
        if spf[i] == 0:
            for j in range(i, max_val + 1, i):
                if spf[j] == 0:
                    spf[j] = i
  
    # Rest of the algorithm...

4. Not Handling Edge Cases

Pitfall: Forgetting to handle special cases like:

  • Arrays with single element (already non-decreasing)
  • Arrays with all prime numbers
  • Numbers less than 2 in the array

Solution: Add proper edge case handling:

def minOperations(self, nums: List[int]) -> int:
    if len(nums) <= 1:
        return 0
  
    # For numbers < 2, handle separately
    # 1 has no proper divisors, so it stays 1
    # 0 is not a natural number (problem states natural numbers)
  
    operation_count = 0
    for i in range(len(nums) - 2, -1, -1):
        if nums[i] > nums[i + 1]:
            if nums[i] < 2:  # Cannot be reduced
                return -1
            nums[i] = smallest_prime_factor[nums[i]]
            if nums[i] > nums[i + 1]:
                return -1
            operation_count += 1
  
    return operation_count

5. Misunderstanding "Greatest Proper Divisor"

Pitfall: Confusing greatest proper divisor with greatest common divisor (GCD) or thinking it's the number itself divided by 2.

Why it's wrong: The greatest proper divisor is the largest divisor that's strictly less than the number itself. For 12, it's 6 (not 4 or 2). The key insight is that dividing by the greatest proper divisor gives the smallest prime factor.

Solution: Use the mathematical property directly: for any composite number n, n / (greatest proper divisor) = smallest prime factor. This is why the preprocessing computes smallest prime factors rather than trying to find greatest proper divisors directly.

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

Which of the following problems can be solved with backtracking (select multiple)


Recommended Readings

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

Load More