Facebook Pixel

2601. Prime Subtraction Operation

Problem Description

You have an array of integers nums with 0-based indexing and length n.

You can perform a special operation on the array multiple times:

  • Choose any index i that hasn't been chosen before
  • Select a prime number p that is strictly less than nums[i]
  • Subtract p from nums[i]

Your goal is to determine if you can transform the array into a strictly increasing array using these operations. A strictly increasing array means each element must be strictly greater than the element before it (i.e., nums[0] < nums[1] < nums[2] < ... < nums[n-1]).

The key constraints are:

  • Each index can only be picked once for the operation
  • The prime number chosen must be strictly less than the current value at that index
  • You want to make the array strictly increasing (no equal consecutive elements allowed)

Return true if it's possible to achieve a strictly increasing array through these operations, and false otherwise.

For example, if you have nums = [4, 9, 6, 10], you could:

  • Pick index 0 and subtract prime 3 from nums[0] to get [1, 9, 6, 10]
  • Pick index 2 and subtract prime 3 from nums[2] to get [1, 9, 3, 10]
  • This would not work since 3 < 9 but we need strictly increasing

The solution works backwards from the end of the array, trying to make each element as large as possible while still being less than the next element, by subtracting the smallest suitable prime.

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

Intuition

The key insight is to work backwards from the end of the array. Why backwards? Because we want to maximize the values of earlier elements to give us the most flexibility.

Think about it this way: if we process from left to right, we might subtract too much from an early element, making it impossible to maintain strict increasing order later. But if we work from right to left, we know exactly what constraint each element must satisfy - it must be less than the element to its right.

For each element nums[i], we need nums[i] < nums[i+1]. If this condition already holds, we don't need to do anything - keeping nums[i] as large as possible gives more room for elements to its left.

If nums[i] >= nums[i+1], we must subtract a prime from nums[i]. The question is: which prime? We need the smallest prime p such that after subtracting it, nums[i] - p < nums[i+1]. This means we need p > nums[i] - nums[i+1].

Why the smallest such prime? Because we want to keep nums[i] as large as possible after the operation. A larger nums[i] makes it easier for nums[i-1] to be less than nums[i].

The binary search comes naturally here - we precompute all primes up to the maximum value in the array, then use binary search to quickly find the smallest prime greater than nums[i] - nums[i+1].

If we can't find such a prime (either because no prime is large enough, or the required prime is >= nums[i]), then it's impossible to make the array strictly increasing, and we return false.

This greedy approach of maximizing each element while satisfying the constraint works because each index can only be operated on once - there's no second chance to adjust a value.

Learn more about Greedy, Math and Binary Search patterns.

Solution Approach

The implementation consists of two main parts: preprocessing prime numbers and processing the array from right to left.

Step 1: Generate Prime Numbers

First, we need to generate all prime numbers up to max(nums). The code uses a simple sieve-like approach:

p = []
for i in range(2, max(nums)):
    for j in p:
        if i % j == 0:
            break
    else:
        p.append(i)

For each number i from 2 to max(nums) - 1, we check if it's divisible by any previously found prime. If not divisible by any prime in p, then i itself is prime and gets added to the list.

Step 2: Process Array from Right to Left

We iterate through the array from the second-to-last element backwards to the first element:

for i in range(n - 2, -1, -1):
    if nums[i] < nums[i + 1]:
        continue

If nums[i] < nums[i + 1], the strict increasing condition is already satisfied, so we skip to the next element.

Step 3: Find and Apply the Smallest Valid Prime

When nums[i] >= nums[i + 1], we need to subtract a prime from nums[i]:

j = bisect_right(p, nums[i] - nums[i + 1])

We use bisect_right to find the smallest prime greater than nums[i] - nums[i + 1]. This gives us the index j of the first prime that, when subtracted from nums[i], will make it less than nums[i + 1].

Step 4: Validation and Update

if j == len(p) or p[j] >= nums[i]:
    return False
nums[i] -= p[j]

We check two failure conditions:

  • j == len(p): No prime is large enough to satisfy our requirement
  • p[j] >= nums[i]: The required prime is not strictly less than nums[i]

If either condition is true, it's impossible to make the array strictly increasing, so we return False.

Otherwise, we subtract p[j] from nums[i] and continue processing.

Time Complexity: O(m√m + n log m) where m = max(nums) and n = len(nums). The prime generation takes O(m√m) and for each element, we perform a binary search in O(log m).

Space Complexity: O(π(m)) where π(m) is the prime counting function, representing the number of primes less than m.

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 = [5, 8, 3].

Step 1: Generate primes up to max(nums) = 8

  • Primes less than 8: [2, 3, 5, 7]

Step 2: Process from right to left

Index i = 1 (comparing nums[1] = 8 with nums[2] = 3):

  • Is 8 < 3? No, so we need to subtract a prime from nums[1]
  • We need nums[1] - p < 3, which means p > 8 - 3 = 5
  • Using binary search on [2, 3, 5, 7] to find smallest prime > 5
  • bisect_right([2, 3, 5, 7], 5) returns index 3 (pointing to 7)
  • Check: Is 7 < 8? Yes, valid!
  • Subtract: nums[1] = 8 - 7 = 1
  • Array becomes: [5, 1, 3]

Index i = 0 (comparing nums[0] = 5 with nums[1] = 1):

  • Is 5 < 1? No, so we need to subtract a prime from nums[0]
  • We need nums[0] - p < 1, which means p > 5 - 1 = 4
  • Using binary search on [2, 3, 5, 7] to find smallest prime > 4
  • bisect_right([2, 3, 5, 7], 4) returns index 2 (pointing to 5)
  • Check: Is 5 < 5? No! The prime must be strictly less than nums[0]
  • Since we can't find a valid prime, return False

The array cannot be made strictly increasing because we would need to subtract at least 5 from nums[0] = 5, but the prime must be strictly less than 5.

Let's try another example where it works: nums = [6, 9, 12]

Step 1: Generate primes up to 12

  • Primes: [2, 3, 5, 7, 11]

Step 2: Process from right to left

Index i = 1 (comparing nums[1] = 9 with nums[2] = 12):

  • Is 9 < 12? Yes, already satisfied. Continue.
  • Array remains: [6, 9, 12]

Index i = 0 (comparing nums[0] = 6 with nums[1] = 9):

  • Is 6 < 9? Yes, already satisfied. Continue.
  • Array remains: [6, 9, 12]

All elements are already strictly increasing, so return True.

Solution Implementation

1from typing import List
2import bisect
3
4class Solution:
5    def primeSubOperation(self, nums: List[int]) -> bool:
6        # Generate all prime numbers less than the maximum value in nums
7        primes = []
8        for candidate in range(2, max(nums)):
9            # Check if candidate is divisible by any existing prime
10            for prime in primes:
11                if candidate % prime == 0:
12                    break
13            else:
14                # If no prime divides candidate, it's prime
15                primes.append(candidate)
16      
17        # Process array from right to left to make it strictly increasing
18        n = len(nums)
19        for i in range(n - 2, -1, -1):
20            # If current element is already less than next, continue
21            if nums[i] < nums[i + 1]:
22                continue
23          
24            # Find the smallest prime that makes nums[i] < nums[i + 1]
25            # We need nums[i] - prime < nums[i + 1]
26            # So prime > nums[i] - nums[i + 1]
27            min_prime_needed = nums[i] - nums[i + 1]
28            prime_index = bisect.bisect_right(primes, min_prime_needed)
29          
30            # Check if a valid prime exists
31            if prime_index == len(primes) or primes[prime_index] >= nums[i]:
32                # No valid prime found (either no prime large enough or prime >= nums[i])
33                return False
34          
35            # Subtract the prime from current element
36            nums[i] -= primes[prime_index]
37      
38        return True
39
1class Solution {
2    public boolean primeSubOperation(int[] nums) {
3        // Generate all prime numbers up to 1000 using trial division
4        List<Integer> primes = new ArrayList<>();
5        for (int i = 2; i <= 1000; ++i) {
6            boolean isPrime = true;
7            // Check if i is divisible by any previously found prime
8            for (int prime : primes) {
9                if (i % prime == 0) {
10                    isPrime = false;
11                    break;
12                }
13            }
14            // If i is prime, add it to the list
15            if (isPrime) {
16                primes.add(i);
17            }
18        }
19      
20        int n = nums.length;
21        // Process array from right to left to ensure strictly increasing order
22        for (int i = n - 2; i >= 0; --i) {
23            // If current element is already less than next element, skip
24            if (nums[i] < nums[i + 1]) {
25                continue;
26            }
27          
28            // Find the smallest prime greater than the difference needed
29            // We need nums[i] - prime < nums[i + 1], so prime > nums[i] - nums[i + 1]
30            int minPrimeNeeded = nums[i] - nums[i + 1];
31            int primeIndex = binarySearchFirstGreater(primes, minPrimeNeeded);
32          
33            // Check if a valid prime exists that can be subtracted
34            if (primeIndex == primes.size() || primes.get(primeIndex) >= nums[i]) {
35                return false;
36            }
37          
38            // Subtract the prime from current element
39            nums[i] -= primes.get(primeIndex);
40        }
41      
42        return true;
43    }
44  
45    /**
46     * Binary search to find the index of the first element greater than target
47     * @param nums sorted list of integers
48     * @param target the value to compare against
49     * @return index of first element > target, or nums.size() if none exists
50     */
51    private int binarySearchFirstGreater(List<Integer> nums, int target) {
52        int left = 0;
53        int right = nums.size();
54      
55        while (left < right) {
56            int mid = (left + right) >> 1;
57            if (nums.get(mid) > target) {
58                right = mid;
59            } else {
60                left = mid + 1;
61            }
62        }
63      
64        return left;
65    }
66}
67
1class Solution {
2public:
3    bool primeSubOperation(vector<int>& nums) {
4        // Build a list of all prime numbers up to 1000
5        vector<int> primes;
6        for (int num = 2; num <= 1000; ++num) {
7            bool isPrime = true;
8          
9            // Check if num is divisible by any previously found prime
10            for (int prime : primes) {
11                if (num % prime == 0) {
12                    isPrime = false;
13                    break;
14                }
15            }
16          
17            // If num is prime, add it to our prime list
18            if (isPrime) {
19                primes.push_back(num);
20            }
21        }
22      
23        int n = nums.size();
24      
25        // Process array from right to left
26        for (int i = n - 2; i >= 0; --i) {
27            // If current element is already less than next element, skip
28            if (nums[i] < nums[i + 1]) {
29                continue;
30            }
31          
32            // Find the smallest prime greater than (nums[i] - nums[i + 1])
33            // This ensures nums[i] - prime < nums[i + 1]
34            int primeIndex = upper_bound(primes.begin(), primes.end(), 
35                                       nums[i] - nums[i + 1]) - primes.begin();
36          
37            // Check if a valid prime exists that can be subtracted
38            if (primeIndex == primes.size() || primes[primeIndex] >= nums[i]) {
39                return false;  // Cannot make array strictly increasing
40            }
41          
42            // Subtract the prime from current element
43            nums[i] -= primes[primeIndex];
44        }
45      
46        return true;  // Successfully made array strictly increasing
47    }
48};
49
1/**
2 * Determines if we can make the array strictly increasing by subtracting prime numbers
3 * @param nums - The input array of numbers
4 * @returns true if the array can be made strictly increasing, false otherwise
5 */
6function primeSubOperation(nums: number[]): boolean {
7    // Generate all prime numbers up to 1000 using Sieve-like approach
8    const primes: number[] = [];
9    for (let i = 2; i <= 1000; i++) {
10        let isPrime = true;
11      
12        // Check if i is divisible by any previously found prime
13        for (const prime of primes) {
14            if (i % prime === 0) {
15                isPrime = false;
16                break;
17            }
18        }
19      
20        // If i is prime, add it to our list
21        if (isPrime) {
22            primes.push(i);
23        }
24    }
25  
26    /**
27     * Binary search to find the index of the smallest prime greater than target
28     * @param target - The target value to search for
29     * @returns The index of the smallest prime greater than target
30     */
31    const findSmallestPrimeGreaterThan = (target: number): number => {
32        let left = 0;
33        let right = primes.length;
34      
35        // Binary search for the leftmost position where prime > target
36        while (left < right) {
37            const mid = Math.floor((left + right) / 2);
38          
39            if (primes[mid] > target) {
40                right = mid;
41            } else {
42                left = mid + 1;
43            }
44        }
45      
46        return left;
47    };
48  
49    const arrayLength = nums.length;
50  
51    // Process array from right to left
52    for (let i = arrayLength - 2; i >= 0; i--) {
53        // If current element is already less than next element, skip
54        if (nums[i] < nums[i + 1]) {
55            continue;
56        }
57      
58        // Find the smallest prime greater than the difference
59        const difference = nums[i] - nums[i + 1];
60        const primeIndex = findSmallestPrimeGreaterThan(difference);
61      
62        // Check if we found a valid prime to subtract
63        if (primeIndex === primes.length || primes[primeIndex] >= nums[i]) {
64            // No valid prime exists to make nums[i] < nums[i + 1]
65            return false;
66        }
67      
68        // Subtract the prime from current element
69        nums[i] -= primes[primeIndex];
70    }
71  
72    return true;
73}
74

Time and Space Complexity

The time complexity of this solution is O(m√m + n log p) where m = max(nums), n = len(nums), and p is the number of primes less than m.

Breaking down the time complexity:

  1. Prime generation: The outer loop runs O(m) times. For each number i, we check divisibility against all previously found primes. In the worst case, we check up to O(√i) primes (since if a number has a factor, it must have one ≤ √i). This gives us O(m√m) for generating all primes up to max(nums).

  2. Main operation loop: The loop runs O(n) times. Each iteration performs a bisect_right operation on the prime list, which takes O(log p) time where p ≈ m/ln(m) by the prime number theorem. This gives us O(n log p).

The overall time complexity is O(m√m + n log p). Since p < m, we can simplify this to O(m√m + n log m). When m is treated as bounded by the input values (not growing with n), this can be approximated as O(n log n) for the dominant operation on the array.

The space complexity is O(p) for storing the prime numbers, where p ≈ m/ln(m). Since m = max(nums) and could be proportional to n in many cases, the space complexity can be expressed as O(n) when considering the relationship between the maximum value and array length.

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

Common Pitfalls

1. Working Forward Instead of Backward

A natural instinct is to process the array from left to right, trying to make each element as small as possible. However, this greedy approach fails because you don't know what constraints future elements will impose.

Why it fails: If you make early elements too small, you might not have enough "room" for later elements to be strictly increasing.

Example:

  • nums = [5, 8, 3]
  • Working forward: Subtract 3 from nums[0] → [2, 8, 3]
  • Now you're stuck - you can't make 3 > 8

Solution: Work backward from the end. This way, you know exactly what constraint each element must satisfy (being less than the next element), and you can maximize each element's value while meeting that constraint.

2. Using <= Instead of < for Prime Selection

The problem states the prime must be strictly less than nums[i], but it's easy to accidentally use <= in the condition check.

Incorrect:

if prime_index == len(primes) or primes[prime_index] > nums[i]:  # Wrong!
    return False

Correct:

if prime_index == len(primes) or primes[prime_index] >= nums[i]:  # Correct
    return False

3. Incorrect Binary Search Target

When finding the minimum prime to subtract, the calculation nums[i] - nums[i + 1] gives us the minimum difference needed. We need a prime strictly greater than this value, not greater than or equal to.

Why bisect_right is correct:

  • If nums[i] = 10 and nums[i + 1] = 5
  • We need prime p such that 10 - p < 5, so p > 5
  • bisect_right(primes, 5) gives us the first prime strictly greater than 5

Common mistake: Using bisect_left would give us primes >= 5, which might include 5 itself, leading to 10 - 5 = 5, violating the strictly increasing requirement.

4. Not Handling the Case Where No Operation is Needed

The code correctly handles this by checking if nums[i] < nums[i + 1]: continue, but forgetting this check would cause unnecessary operations and potentially invalid results.

Example: nums = [1, 2, 3] is already strictly increasing. Without the check, you might try to subtract primes unnecessarily and make the array invalid.

5. Inefficient Prime Generation

While the given implementation works, using a more efficient sieve like the Sieve of Eratosthenes would be better for larger inputs:

def generate_primes(limit):
    if limit < 2:
        return []
    sieve = [True] * limit
    sieve[0] = sieve[1] = False
    for i in range(2, int(limit**0.5) + 1):
        if sieve[i]:
            for j in range(i*i, limit, i):
                sieve[j] = False
    return [i for i in range(2, limit) if sieve[i]]

This runs in O(m log log m) time versus the O(m√m) of the trial division method.

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

Problem: Given a list of tasks and a list of requirements, compute a sequence of tasks that can be performed, such that we complete every task once while satisfying all the requirements.

Which of the following method should we use to solve this problem?


Recommended Readings

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

Load More