Maximize Count of Distinct Primes After Split
HardPrompt
You are given an integer array nums having length n and a 2D integer array queries where queries[i] = [idx, val].
For each query:
- Update
nums[idx] = val. - Choose an integer
kwith1 <= k < nto split the array into the non-empty prefixnums[0..k-1]and suffixnums[k..n-1]such that the sum of the counts of distinct prime values in each part is maximum.
Note: The changes made to the array in one query persist into the next query.
Return an array containing the result for each query, in the order they are given.
Example 1:
Input: nums = [2,1,3,1,2], queries = [[1,2],[3,3]]
Output: [3,4]
Explanation:
- Initially
nums = [2, 1, 3, 1, 2]. - After 1st query,
nums = [2, 2, 3, 1, 2]. Splitnumsinto[2]and[2, 3, 1, 2].[2]consists of 1 distinct prime and[2, 3, 1, 2]consists of 2 distinct primes. Hence, the answer for this query is1 + 2 = 3. - After 2nd query,
nums = [2, 2, 3, 3, 2]. Splitnumsinto[2, 2, 3]and[3, 2]with an answer of2 + 2 = 4. - The output is
[3, 4].
Example 2:
Input: nums = [2,1,4], queries = [[0,1]]
Output: [0]
Explanation:
- Initially
nums = [2, 1, 4]. - After 1st query,
nums = [1, 1, 4]. There are no prime numbers innums, hence the answer for this query is 0. - The output is
[0].
Constraints:
2 <= n == nums.length <= 5 * 1041 <= queries.length <= 5 * 1041 <= nums[i] <= 1050 <= queries[i][0] < nums.length1 <= queries[i][1] <= 105
Approaches
3 approaches with complexity analysis and trade-offs.
This is a straightforward, brute-force approach that directly simulates the process described in the problem. For each query, after updating the array, it iterates through every possible split point k. For each split, it computes the distinct prime counts for the resulting prefix and suffix subarrays from scratch and sums them up. The maximum sum found across all splits is the answer for that query.
Algorithm
- Pre-compute prime factors for all numbers up to the maximum possible value using a Sieve of Eratosthenes.
- For each query
[idx, val]:- Update the array:
nums[idx] = val. - Initialize a variable
max_scoreto 0. - Iterate through all possible split points
kfrom1ton-1. - For each
k: a. Calculate the number of distinct prime factors in the prefixnums[0...k-1]. This involves creating a set, iterating through the prefix elements, finding their prime factors, and adding them to the set. The size of the set is the count for the prefix. b. Similarly, calculate the number of distinct prime factors for the suffixnums[k...n-1]. c. The score for the splitkis the sum of the two counts. d. Updatemax_score = max(max_score, score). - After checking all
k, themax_scoreis the result for the current query. Add it to the results array.
- Update the array:
Walkthrough
The core of this method is a nested loop structure for each query. The outer loop iterates through all valid split points k from 1 to n-1. The inner loops are responsible for calculating the distinct prime counts. To do this, we use a HashSet to keep track of unique prime factors. For a given prefix nums[0...k-1], we iterate from i=0 to k-1, find all prime factors of nums[i], and add them to a prefix_primes set. The size of this set gives the count. We do the same for the suffix nums[k...n-1]. This entire process is repeated for every single query.
// PrimeHandler class for Sieve and factorization is assumed to exist.PrimeHandler primeHandler = new PrimeHandler(); // Inside the main method, for each query:// nums is updated with queries[i][1] at index queries[i][0] int maxScore = 0;int n = nums.length;for (int k = 1; k < n; k++) { // Calculate distinct primes in prefix nums[0...k-1] Set<Integer> prefixPrimes = new HashSet<>(); for (int i = 0; i < k; i++) { prefixPrimes.addAll(primeHandler.getPrimeFactors(nums[i])); } // Calculate distinct primes in suffix nums[k...n-1] Set<Integer> suffixPrimes = new HashSet<>(); for (int i = k; i < n; i++) { suffixPrimes.addAll(primeHandler.getPrimeFactors(nums[i])); } maxScore = Math.max(maxScore, prefixPrimes.size() + suffixPrimes.size());}// maxScore is the answer for the current query.Complexity
Time
O(Q * N^2 * log(V)), where `Q` is the number of queries, `N` is the length of `nums`, and `V` is the maximum value. For each of the `Q` queries, we iterate `N` times for the split point `k`. Inside this loop, we iterate up to `N` times again to build the prime sets, and factorization takes `O(log V)`.
Space
O(N * log(V)) per query, where `V` is the maximum value of a number in `nums`. This space is used to store the sets of prime factors for the prefix and suffix.
Trade-offs
Pros
Simple to understand and implement.
Correctly solves the problem for very small inputs.
Cons
Extremely inefficient due to nested loops.
Repeatedly calculates prime factors for the same numbers across different split points and queries.
Will result in a 'Time Limit Exceeded' error for the given constraints.
This approach improves upon the brute-force method by optimizing the calculation of scores for all split points. Instead of re-calculating from scratch for each split k, it pre-computes the distinct prime counts for all possible prefixes and suffixes in two separate linear passes. After these pre-computations, it finds the maximum score by iterating through the split points one more time.
Algorithm
- Pre-compute prime factors using a Sieve.
- For each query
[idx, val]:- Update
nums[idx] = val. - Create a
prefix_countsarray of sizen+1. Populate it by iterating fromk=1ton. For eachk, calculate the distinct prime count innums[0...k-1]by adding the prime factors ofnums[k-1]to a running set of primes. - Create a
suffix_countsarray of sizen+1. Populate it by iterating fromk=n-1down to0. For eachk, calculate the distinct prime count innums[k...n-1]by adding factors ofnums[k]to a running set. - Initialize
max_score = 0. - Iterate
kfrom1ton-1: a. The score for the split isprefix_counts[k] + suffix_counts[k]. b. Updatemax_score = max(max_score, score). - Store
max_scoreas the answer for the query.
- Update
Walkthrough
For each query, after the array update, we first build an array prefix_counts. prefix_counts[k] will store the number of distinct prime factors in nums[0...k-1]. This can be computed efficiently in a single pass from left to right, maintaining a running HashSet of primes. Similarly, we build a suffix_counts array where suffix_counts[k] stores the count for nums[k...n-1], which is done with a pass from right to left. Once we have these two arrays, we can find the score for any split k in O(1) time by summing prefix_counts[k] and suffix_counts[k]. A final loop over k finds the maximum possible score.
// PrimeHandler class is assumed to exist.PrimeHandler primeHandler = new PrimeHandler(); // Inside the main method, for each query:// nums is updated.int n = nums.length; // Pre-calculate all prefix countsint[] prefixCounts = new int[n + 1];Set<Integer> currentPrefixPrimes = new HashSet<>();for (int k = 1; k <= n; k++) { currentPrefixPrimes.addAll(primeHandler.getPrimeFactors(nums[k - 1])); prefixCounts[k] = currentPrefixPrimes.size();} // Pre-calculate all suffix countsint[] suffixCounts = new int[n + 1];Set<Integer> currentSuffixPrimes = new HashSet<>();for (int k = n - 1; k >= 0; k--) { currentSuffixPrimes.addAll(primeHandler.getPrimeFactors(nums[k])); suffixCounts[k] = currentSuffixPrimes.size();} // Find the best splitint maxScore = 0;for (int k = 1; k < n; k++) { // Score for split at k is count for prefix nums[0...k-1] and suffix nums[k...n-1] int score = prefixCounts[k] + suffixCounts[k]; maxScore = Math.max(maxScore, score);}// maxScore is the answer for the current query.Complexity
Time
O(Q * N * log(V)). For each query, we perform two passes over the `N` elements to build the count arrays, with factorization at each step. This is still too slow for the given constraints.
Space
O(N * log(V)), primarily for storing the prime factors of all numbers and the prefix/suffix count arrays.
Trade-offs
Pros
Significantly faster than the pure brute-force approach.
Avoids redundant computations within a single query processing.
Cons
Still too slow for the given constraints as it performs linear scans for each query.
The prefix and suffix count arrays are completely recomputed for every query, which is inefficient.
This highly efficient approach reframes the problem to leverage a powerful data structure. The key insight is that the score for a split k, count(prefix) + count(suffix), is equal to count(total) + count(intersection). Since count(total) (the total number of distinct primes in the array) is independent of the split point k, we only need to maximize the size of the intersection, i.e., the number of primes that appear in both the prefix and the suffix. A prime p is in the intersection if its first occurrence is before k and its last is at or after k. This property allows us to use a segment tree to maintain the intersection counts for all k. An update to the array only affects a small number of primes, leading to a few efficient updates on the segment tree.
Algorithm
- Preprocessing:
- Use a Sieve to find the Smallest Prime Factor (SPF) for all numbers up to
10^5. - Create a map
occurrenceswhereoccurrences[p]is a sorted set of indicesiwherepis a prime factor ofnums[i]. - Calculate
total_distinct_primes, the total number of unique primes across the entire array.
- Use a Sieve to find the Smallest Prime Factor (SPF) for all numbers up to
- Data Structure:
- Use a Segment Tree with Lazy Propagation over the range of split points
kfrom1ton-1. This tree will support range additions and range maximum queries.
- Use a Segment Tree with Lazy Propagation over the range of split points
- Initialization:
- The score for a split
kistotal_distinct_primes + |P_prefix ∩ P_suffix|. We need to maximize the size of the intersection, which we calloverlap(k). - A prime
pcontributes tooverlap(k)if its first occurrencefis< kand its last occurrencelis>= k. This is true forkin the range[f+1, l]. - For each prime
pthat appears more than once, perform a range update on the segment tree, adding1to the range[f+1, l].
- The score for a split
- Per Query:
- For an update
nums[idx] = val, identify the prime factors of the old and new values (P_old,P_new). - For each prime
pinP_old ∪ P_new: a. Undo: Ifphad multiple occurrences before, find its oldfirstandlastindices (f,l) and subtract1from the range[f+1, l]in the segment tree. b. Update: Modifyoccurrences[p]by removing/addingidx. Updatetotal_distinct_primesif the set of occurrences forpbecomes empty or non-empty. c. Redo: Ifphas multiple occurrences now, find its newfirstandlastindices (f',l') and add1to the range[f'+1, l']in the segment tree. - After processing all affected primes, query the segment tree for the maximum value,
max_overlap. - The answer for the query is
total_distinct_primes + max_overlap.
- For an update
Walkthrough
We maintain the first and last occurrence indices for each prime. A prime p with first occurrence f and last l contributes to the intersection (overlap) for all splits k where f < k <= l. This means for each such prime, we can think of it as adding +1 to an 'overlap score' for all k in the range [f+1, l]. A segment tree with lazy propagation is perfect for this: it can handle range additions and find the maximum value in a range efficiently.
When nums[idx] is updated, only the prime factors of the old and new values are affected. For each such prime, we find its first and last occurrences before the change, undo its contribution on the segment tree (a range update of -1), update its set of occurrences, and then apply its new contribution (a range update of +1 based on new first and last indices). The total number of distinct primes is also tracked separately. The final answer for a query is this total count plus the maximum overlap score queried from the segment tree.
// Segment Tree with lazy propagation for range add and range max queryclass SegmentTree { /* ... standard implementation ... */ } // Main logic for a queryvoid processQuery(int idx, int val) { int oldVal = nums[idx]; Set<Integer> oldPrimes = primeHandler.getPrimeFactors(oldVal); Set<Integer> newPrimes = primeHandler.getPrimeFactors(val); Set<Integer> affectedPrimes = new HashSet<>(oldPrimes); affectedPrimes.addAll(newPrimes); for (int p : affectedPrimes) { TreeSet<Integer> occs = occurrences.get(p); // 1. Undo old contribution if (occs != null && occs.size() > 1) { int oldFirst = occs.first(); int oldLast = occs.last(); segTree.rangeUpdate(oldFirst + 1, oldLast, -1); } // 2. Update occurrences and totalDistinctPrimes if (oldPrimes.contains(p)) { if (occs.size() == 1) totalDistinctPrimes--; occs.remove(idx); } if (newPrimes.contains(p)) { if (occs == null) { occs = new TreeSet<>(); occurrences.put(p, occs); } if (occs.isEmpty()) totalDistinctPrimes++; occs.add(idx); } // 3. Add new contribution if (occs != null && occs.size() > 1) { int newFirst = occs.first(); int newLast = occs.last(); segTree.rangeUpdate(newFirst + 1, newLast, 1); } } nums[idx] = val; int maxOverlap = segTree.queryMax(1, n - 1); int result = totalDistinctPrimes + Math.max(0, maxOverlap); // Add result to answer list}Complexity
Time
O(V log log V + (N + π(V) + Q * log V) * log N). Sieve is `O(V log log V)`. Initialization involves `O(N log V)` for factorization and `O(π(V) * log N)` for populating the segment tree (where `π(V)` is the number of primes up to `V`). Each of the `Q` queries takes `O(log V * log N)` because we update for a few primes (`~log V`), and each update involves `TreeSet` operations (`log N`) and segment tree updates (`log N`).
Space
O(V + N log(V)). `O(V)` for the Sieve. The `occurrences` map stores each appearance of a prime factor, totaling `O(N log V)` space in the worst case. The segment tree requires `O(N)` space.
Trade-offs
Pros
Very efficient, fast enough to pass the given constraints.
Updates are handled logarithmically, not linearly.
Cons
Significantly more complex to understand and implement correctly.
Requires careful handling of edge cases and state changes for the
occurrencesmap and segment tree.
Video 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.