3326. Minimum Division Operations to Make Array Non Decreasing
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:
- Select any one element from
nums
- 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, sop / 1 = p
(no change) - For a composite number
n
, dividing by its greatest proper divisor gives you the smallest prime factor ofn
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.
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.
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
, theni
is prime (hasn't been marked by any smaller prime) - For each prime
i
, we mark all its multiplesj
withlpf[j] = i
(but only ifj
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:
- Start from the second-to-last element and move leftward
- At each position
i
, check ifnums[i] > nums[i + 1]
- If yes, we must reduce
nums[i]
by replacing it with its smallest prime factor:nums[i] = lpf[nums[i]]
- 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
- If still greater, return
- 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)
wherem
is the length of the input array - Overall:
O(n log log n + m)
wheren
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 EvaluatorExample 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.
Which of the following problems can be solved with backtracking (select multiple)
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
Math for Technical Interviews How much math do I need to know for technical interviews The short answer is about high school level math Computer science is often associated with math and some universities even place their computer science department under the math faculty However the reality is that you
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
Want a Structured Path to Master System Design Too? Don’t Miss This!