Prime Subtraction Operation
MedPrompt
You are given a 0-indexed integer array nums of length n.
You can perform the following operation as many times as you want:
- Pick an index
ithat you haven’t picked before, and pick a primepstrictly less thannums[i], then subtractpfromnums[i].
Return true if you can make nums a strictly increasing array using the above operation and false otherwise.
A strictly increasing array is an array whose each element is strictly greater than its preceding element.
Example 1:
Input: nums = [4,9,6,10]
Output: true
Explanation: In the first operation: Pick i = 0 and p = 3, and then subtract 3 from nums[0], so that nums becomes [1,9,6,10].
In the second operation: i = 1, p = 7, subtract 7 from nums[1], so nums becomes equal to [1,2,6,10].
After the second operation, nums is sorted in strictly increasing order, so the answer is true.Example 2:
Input: nums = [6,8,11,12]
Output: true
Explanation: Initially nums is sorted in strictly increasing order, so we don't need to make any operations.Example 3:
Input: nums = [5,8,3]
Output: false
Explanation: It can be proven that there is no way to perform operations to make nums sorted in strictly increasing order, so the answer is false.
Constraints:
1 <= nums.length <= 10001 <= nums[i] <= 1000nums.length == n
Approaches
2 approaches with complexity analysis and trade-offs.
A more efficient approach is to use a greedy strategy. When processing the array from left to right, we make a locally optimal choice at each step. The key insight is that to make it easiest for the subsequent elements to be larger, we should make the current element as small as possible, while still being greater than the previous element.
Algorithm
- Pre-compute all primes up to 1001 using a Sieve.
- Create an auxiliary array,
largestPrimeSmallerThan, of size 1002.largestPrimeSmallerThan[k]will store the largest prime strictly less thank. This can be filled inO(M)time after the sieve. - Initialize a variable
prev = 0. - Iterate through
numsfromi = 0ton-1:- If
nums[i] <= prev, returnfalse. - Calculate the difference:
diff = nums[i] - prev. - Find the largest prime
pless thandiffusing the pre-computed array:p = largestPrimeSmallerThan[diff]. - If a valid prime
p > 0is found, update the current element's value for the next iteration:current_val = nums[i] - p. - Otherwise, we cannot subtract any prime, so
current_val = nums[i]. - Update
prev = current_val.
- If
- If the loop completes, return
true.
Walkthrough
The core idea is to iterate through the nums array from left to right, maintaining the value of the 'previous' element in the modified, strictly increasing sequence. Let's call this prev. Initially, prev can be considered 0.
For each element nums[i], we first check if it's possible to make it larger than prev. If nums[i] is already less than or equal to prev, it's impossible. This is because any subtraction will only make nums[i] smaller, so it can never become greater than prev. In this case, we can immediately return false.
If nums[i] > prev, we have a valid starting point. To be greedy, we want to modify nums[i] to be the smallest possible value that is still greater than prev. This is achieved by subtracting the largest possible prime p from nums[i] such that the result nums[i] - p is still greater than prev.
This condition nums[i] - p > prev is equivalent to p < nums[i] - prev. So, we need to find the largest prime p that is strictly less than the difference nums[i] - prev.
If such a prime exists, we subtract it from nums[i] to get the new value for the current position. If no such prime exists (e.g., if nums[i] - prev is 2 or less), we cannot make nums[i] any smaller, so we leave it as is.
We then update prev to this new, possibly modified value of nums[i] and proceed to the next element.
If we successfully process the entire array, it means a valid sequence can be formed, and we return true.
To efficiently find the 'largest prime less than k', we can pre-compute this information for all numbers up to the maximum possible value.
import java.util.Arrays; class Solution { public boolean primeSubtraction(int[] nums) { int maxVal = 1002; // Max possible value for num or diff + buffer boolean[] isPrime = new boolean[maxVal]; Arrays.fill(isPrime, true); isPrime[0] = isPrime[1] = false; for (int p = 2; p * p < maxVal; p++) { if (isPrime[p]) { for (int i = p * p; i < maxVal; i += p) { isPrime[i] = false; } } } int[] largestPrimeSmallerThan = new int[maxVal]; // largestPrimeSmallerThan[k] stores the largest prime < k for (int i = 3; i < maxVal; i++) { if (isPrime[i - 1]) { largestPrimeSmallerThan[i] = i - 1; } else { largestPrimeSmallerThan[i] = largestPrimeSmallerThan[i - 1]; } } int prev = 0; for (int num : nums) { if (num <= prev) { return false; } int diff = num - prev; int p = largestPrimeSmallerThan[diff]; if (p > 0) { prev = num - p; } else { prev = num; } } return true; }}Complexity
Time
O(N + M log log M), where `N` is the length of `nums` and `M` is the maximum value (1000). The pre-computation takes `O(M log log M)` for the sieve and `O(M)` for the helper array. The main loop runs in `O(N)` time.
Space
O(M) to store the prime information and the `largestPrimeSmallerThan` array, where M is the maximum possible value in `nums` (around 1000).
Trade-offs
Pros
Very efficient in both time and space.
The greedy choice is simple and proven to be optimal for this problem.
Cons
The correctness of the greedy strategy is not immediately obvious without careful reasoning.
This approach explores all possible valid modifications to the array using recursion and memoization to avoid recomputing results for the same subproblems. A subproblem is defined by the current index being considered and the value of the preceding element.
Algorithm
- Pre-compute all prime numbers up to 1000 using the Sieve of Eratosthenes.
- Initialize a 2D memoization array
memo[n+1][1001]with a value indicating 'not computed'. - Define a recursive function
solve(index, prev_val):- If
index == n, returntrue. - If
memo[index][prev_val]is already computed, return its value. - Try not modifying
nums[index]:- If
nums[index] > prev_valandsolve(index + 1, nums[index])istrue, store and returntrue.
- If
- Try modifying
nums[index]:- Iterate through each prime
p < nums[index]. - Let
new_val = nums[index] - p. - If
new_val > prev_valandsolve(index + 1, new_val)istrue, store and returntrue.
- Iterate through each prime
- If no path returns
true, store and returnfalse.
- If
- The final answer is the result of
solve(0, 0).
Walkthrough
We define a recursive function, say solve(index, prev_val), which returns true if the subarray from index to the end can be made strictly increasing, given the previous element's value is prev_val.
The base case for the recursion is when index reaches the end of the array (n), which means we have successfully built a valid sequence, so we return true.
In the recursive step for solve(index, prev_val), we have two main choices for the current element nums[index]:
- No Operation: If
nums[index]is already strictly greater thanprev_val, we can choose to not modify it. We then check if a solution exists for the rest of the array by callingsolve(index + 1, nums[index]). - Subtraction Operation: We can iterate through all prime numbers
pstrictly less thannums[index]. For each prime, we calculate the new valuenew_val = nums[index] - p. Ifnew_valis strictly greater thanprev_val, we check if a solution exists for the rest of the array by callingsolve(index + 1, new_val).
If any of these recursive calls return true, it means a valid sequence can be formed, and we return true. If all possibilities are exhausted without finding a solution, we return false.
To optimize this recursive exploration, we use a 2D memoization table, memo[index][prev_val], to store the results of subproblems. This avoids redundant calculations and significantly improves performance from exponential to polynomial time.
We would also need a list of primes up to 1000, which can be pre-computed using a sieve.
import java.util.ArrayList;import java.util.Arrays;import java.util.List; class Solution { private List<Integer> primes; private int[][] memo; private int[] nums; private int n; public boolean primeSubtraction(int[] nums) { this.nums = nums; this.n = nums.length; this.memo = new int[n][1001]; for (int[] row : memo) { Arrays.fill(row, -1); } sieve(1000); return solve(0, 0); } private boolean solve(int index, int prevVal) { if (index == n) { return true; } if (memo[index][prevVal] != -1) { return memo[index][prevVal] == 1; } // Option 1: No operation if (nums[index] > prevVal) { if (solve(index + 1, nums[index])) { memo[index][prevVal] = 1; return true; } } // Option 2: Subtract a prime for (int p : primes) { if (p >= nums[index]) { break; } int newVal = nums[index] - p; if (newVal > prevVal) { if (solve(index + 1, newVal)) { memo[index][prevVal] = 1; return true; } } } memo[index][prevVal] = 0; return false; } private void sieve(int maxVal) { primes = new ArrayList<>(); boolean[] isPrime = new boolean[maxVal + 1]; Arrays.fill(isPrime, true); isPrime[0] = isPrime[1] = false; for (int p = 2; p * p <= maxVal; p++) { if (isPrime[p]) { for (int i = p * p; i <= maxVal; i += p) { isPrime[i] = false; } } } for (int p = 2; p <= maxVal; p++) { if (isPrime[p]) { primes.add(p); } } }}Complexity
Time
O(N * M * pi(M)), where `N` is the length of `nums`, `M` is the maximum possible value in `nums` (1000), and `pi(M)` is the number of primes less than `M`. The state space is `N * M`, and for each state, we iterate through up to `pi(M)` primes.
Space
O(N * M + M) for the memoization table and the list of primes, where N is `nums.length` and M is the maximum possible value in `nums`.
Trade-offs
Pros
Guaranteed to find the correct answer by exploring all valid possibilities.
Cons
High time complexity, which might lead to a 'Time Limit Exceeded' error on larger constraints.
High space complexity due to the 2D memoization table.
Solutions
Solution
class Solution {public boolean primeSubOperation(int[] nums) { List<Integer> p = new ArrayList<>(); for (int i = 2; i <= 1000; ++i) { boolean ok = true; for (int j : p) { if (i % j == 0) { ok = false; break; } } if (ok) { p.add(i); } } int n = nums.length; for (int i = n - 2; i >= 0; --i) { if (nums[i] < nums[i + 1]) { continue; } int j = search(p, nums[i] - nums[i + 1]); if (j == p.size() || p.get(j) >= nums[i]) { return false; } nums[i] -= p.get(j); } return true; }private int search(List<Integer> nums, int x) { int l = 0, r = nums.size(); while (l < r) { int mid = (l + r) >> 1; if (nums.get(mid) > x) { r = mid; } else { l = mid + 1; } } return l; }}class Solution {public: bool primeSubOperation(vector<int> &nums) { vector<int> p; for (int i = 2; i <= 1000; ++i) { bool ok = true; for (int j : p) { if (i % j == 0) { ok = false; break; } } if (ok) { p.push_back(i); } } int n = nums.size(); for (int i = n - 2; i >= 0; --i) { if (nums[i] < nums[i + 1]) { continue; } int j = upper_bound(p.begin(), p.end(), nums[i] - nums[i + 1]) - p.begin(); if (j == p.size() || p[j] >= nums[i]) { return false; } nums[i] -= p[j]; } return true; }};class Solution: def primeSubOperation(self, nums: List[int]) -> bool: p = [] for i in range(2, max(nums)): for j in p: if i % j == 0: break else: p . append(i) n = len(nums) for i in range(n - 2, - 1, - 1): if nums[i] < nums[i + 1]: continue j = bisect_right(p, nums[i] - nums[i + 1]) if j == len(p) or p[j] >= nums[i]: return False nums[i] -= p[j] return TrueVideo walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.