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 thannums[i]
- Subtract
p
fromnums[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.
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 requirementp[j] >= nums[i]
: The required prime is not strictly less thannums[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 EvaluatorExample 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 fromnums[1]
- We need
nums[1] - p < 3
, which meansp > 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 fromnums[0]
- We need
nums[0] - p < 1
, which meansp > 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:
-
Prime generation: The outer loop runs
O(m)
times. For each numberi
, we check divisibility against all previously found primes. In the worst case, we check up toO(√i)
primes (since if a number has a factor, it must have one ≤ √i). This gives usO(m√m)
for generating all primes up tomax(nums)
. -
Main operation loop: The loop runs
O(n)
times. Each iteration performs abisect_right
operation on the prime list, which takesO(log p)
time wherep ≈ m/ln(m)
by the prime number theorem. This gives usO(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
andnums[i + 1] = 5
- We need prime
p
such that10 - p < 5
, sop > 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.
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
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
Binary Search Speedrun For each of the Speedrun questions you will be given a binary search related problem and a corresponding multiple choice question The multiple choice questions are related to the techniques and template s introduced in the binary search section It's recommended that you have gone through at
Want a Structured Path to Master System Design Too? Don’t Miss This!