LeetCode 3569 - Maximize Count of Distinct Primes After Split
The task asks us to maintain an integer array that is repeatedly updated, and after each update we must determine an optimal split point that maximizes a specific score based on distinct prime values. For any split index , the array is divided into a prefix and a suffix .
Difficulty: 🔴 Hard
Topics: Array, Math, Segment Tree, Number Theory
Solution
Problem Understanding
The task asks us to maintain an integer array that is repeatedly updated, and after each update we must determine an optimal split point that maximizes a specific score based on distinct prime values.
For any split index $k$, the array is divided into a prefix $[0..k-1]$ and a suffix $[k..n-1]$. We compute the number of distinct prime values in each part and sum them. The goal is to choose $k$ that maximizes this sum after every update query.
The key complication is that updates persist across queries, so the array evolves dynamically. Since both the array size and number of queries can be up to $5 \times 10^4$, recomputing from scratch for every query is infeasible.
The important constraints tell us several things. First, values are bounded by $10^5$, which makes precomputation of primality feasible. Second, the problem is dynamic, meaning we must support efficient point updates and repeated global queries. Third, the split position is not fixed, so we need a structure that can evaluate all possible split points efficiently.
Edge cases include arrays with no primes at all, arrays where all elements are the same prime, and updates that completely remove or introduce a prime. Another subtle case is when a prime appears only once, meaning it cannot contribute to both sides of any split.
Approaches
Brute Force Idea
The naive approach is straightforward. After each query update, we recompute the best split by iterating over every possible $k$, and for each $k$, compute how many distinct primes exist in both prefix and suffix. This requires scanning the array twice per split, or maintaining sets per split, leading to excessive recomputation.
Even if we optimize slightly by precomputing prime occurrences, the need to recompute prefix and suffix distinct prime sets for every query leads to repeated $O(n)$ or worse work per split, making it far too slow.
Key Insight
The key observation is that each distinct prime contributes in a very structured way across split points.
For a given prime $p$, suppose its occurrences lie between indices $L_p$ and $R_p$. Then:
- It contributes 1 to any split where it appears entirely on one side.
- It contributes 2 if the split lies strictly between its first and last occurrence, i.e., $L_p < k \le R_p$, because it appears in both prefix and suffix.
Thus, every prime defines an interval of “bonus contribution” over valid split points. The problem reduces to maintaining a dynamic set of intervals and repeatedly asking: what is the maximum number of overlapping intervals at any split point.
This is a classic segment tree problem with range updates and global maximum queries, combined with dynamic maintenance of intervals as array values change.
Complexity Comparison
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | $O(q \cdot n^2)$ | $O(1)$ | Recomputes split score from scratch |
| Optimal | $O((n + q)\log n)$ | $O(n)$ | Segment tree over split points + dynamic interval tracking |
Algorithm Walkthrough
We transform the problem into maintaining intervals over split positions.
Step 1: Precompute primes
We precompute primality up to $10^5$ using a sieve so we can quickly determine whether a value is relevant.
Step 2: Track occurrences of each value
For every number value $v$, we maintain a sorted structure of indices where it appears. This allows us to quickly determine:
- $L_v$, the first occurrence
- $R_v$, the last occurrence
These endpoints define the contribution interval for that value if it is prime.
Step 3: Maintain global contribution of distinct primes
We maintain a counter $D$, representing how many distinct primes currently exist in the array. This changes when a prime is introduced or completely removed.
Step 4: Convert each prime into a segment update
For a prime value $p$, with occurrence range $[L, R]$, it contributes +1 extra whenever:
$$L < k \le R$$
This corresponds to range update on segment tree over $k \in [L+1, R]$.
Step 5: Segment tree for maximum overlap
We build a segment tree over all valid split positions $k \in [1, n-1]$, supporting:
- Range add updates when intervals change
- Query for maximum overlap across all $k$
Step 6: Handling updates
When updating index i from old value to new value:
- Remove index
ifrom old value’s occurrence set. - Recompute old value’s interval and update segment tree accordingly.
- Insert index
iinto new value’s occurrence set. - Recompute new value’s interval and update segment tree accordingly.
- Update distinct prime count $D$ when a prime appears or disappears entirely.
Step 7: Answer query
After processing updates, the answer is:
$$D + \max(\text{segment tree})$$
Why it works
Each prime contributes exactly 1 baseline occurrence in the array. The segment tree tracks exactly when that prime is split across both sides, adding an extra +1 contribution. Since all primes are independent in terms of contribution intervals, summing them reduces to maintaining overlap counts, and maximizing over all split points yields the correct optimal split.
Python Solution
from typing import List
import bisect
class SegTree:
def __init__(self, n: int):
self.n = n
self.size = 4 * n
self.maxv = [0] * self.size
self.lazy = [0] * self.size
def _push(self, idx: int):
if self.lazy[idx] != 0:
for child in (idx * 2, idx * 2 + 1):
self.maxv[child] += self.lazy[idx]
self.lazy[child] += self.lazy[idx]
self.lazy[idx] = 0
def _update(self, idx: int, l: int, r: int, ql: int, qr: int, val: int):
if ql > r or qr < l:
return
if ql <= l and r <= qr:
self.maxv[idx] += val
self.lazy[idx] += val
return
self._push(idx)
mid = (l + r) // 2
self._update(idx * 2, l, mid, ql, qr, val)
self._update(idx * 2 + 1, mid + 1, r, ql, qr, val)
self.maxv[idx] = max(self.maxv[idx * 2], self.maxv[idx * 2 + 1])
def add(self, l: int, r: int, val: int):
if l <= r:
self._update(1, 1, self.n, l, r, val)
def query_max(self):
return self.maxv[1]
class Solution:
def maximumCount(self, nums: List[int], queries: List[List[int]]) -> List[int]:
n = len(nums)
MAXV = 100000
is_prime = [True] * (MAXV + 1)
is_prime[0] = is_prime[1] = False
for i in range(2, int(MAXV ** 0.5) + 1):
if is_prime[i]:
for j in range(i * i, MAXV + 1, i):
is_prime[j] = False
pos = {v: [] for v in range(MAXV + 1)}
def get_range(v):
if not pos[v]:
return None
return pos[v][0], pos[v][-1]
def apply(v, delta):
if not is_prime[v] or not pos[v]:
return
l, r = pos[v][0], pos[v][-1]
seg.add(l + 1, r, delta)
for i, v in enumerate(nums):
bisect.insort(pos[v], i)
seg = SegTree(n)
distinct = sum(1 for v in pos if pos[v] and is_prime[v])
for v in pos:
if pos[v] and is_prime[v]:
l, r = pos[v][0], pos[v][-1]
seg.add(l + 1, r, 1)
res = []
for idx, val in queries:
old = nums[idx]
if old != val:
if is_prime[old]:
apply(old, -1)
nums[idx] = val
if is_prime[val]:
apply(val, -1)
i = bisect.bisect_left(pos[old], idx)
pos[old].pop(i)
bisect.insort(pos[val], idx)
if is_prime[old]:
if not pos[old]:
distinct -= 1
else:
apply(old, 1)
if is_prime[val]:
if len(pos[val]) == 1:
distinct += 1
apply(val, 1)
res.append(distinct + seg.query_max())
return res
Code Explanation
We first precompute primes up to $10^5$. We maintain a dictionary mapping each value to a sorted list of indices where it appears. The segment tree tracks how many primes "span" each possible split point.
For each update, we carefully remove the old contribution of the affected values and reinsert updated intervals. The segment tree ensures that we can efficiently recompute the best split.
The final answer combines the number of distinct primes with the maximum number of primes that appear on both sides of some split.
Go Solution
package main
import (
"sort"
)
type SegTree struct {
n int
maxv []int
lazy []int
}
func newSegTree(n int) *SegTree {
return &SegTree{
n: n,
maxv: make([]int, 4*n),
lazy: make([]int, 4*n),
}
}
func (st *SegTree) push(idx int) {
if st.lazy[idx] != 0 {
for _, c := range []int{idx * 2, idx*2 + 1} {
st.maxv[c] += st.lazy[idx]
st.lazy[c] += st.lazy[idx]
}
st.lazy[idx] = 0
}
}
func (st *SegTree) update(idx, l, r, ql, qr, val int) {
if ql > r || qr < l {
return
}
if ql <= l && r <= qr {
st.maxv[idx] += val
st.lazy[idx] += val
return
}
st.push(idx)
mid := (l + r) / 2
st.update(idx*2, l, mid, ql, qr, val)
st.update(idx*2+1, mid+1, r, ql, qr, val)
if st.maxv[idx*2] > st.maxv[idx*2+1] {
st.maxv[idx] = st.maxv[idx*2]
} else {
st.maxv[idx] = st.maxv[idx*2+1]
}
}
func (st *SegTree) add(l, r, val int) {
if l <= r {
st.update(1, 1, st.n, l, r, val)
}
}
func (st *SegTree) queryMax() int {
return st.maxv[1]
}
func sieve(maxv int) []bool {
isPrime := make([]bool, maxv+1)
for i := 2; i <= maxv; i++ {
isPrime[i] = true
}
for i := 2; i*i <= maxv; i++ {
if isPrime[i] {
for j := i * i; j <= maxv; j += i {
isPrime[j] = false
}
}
}
return isPrime
}
func maximumCount(nums []int, queries [][]int) []int {
n := len(nums)
isPrime := sieve(100000)
pos := make(map[int][]int)
insert := func(v, idx int) {
arr := pos[v]
arr = append(arr, idx)
sort.Ints(arr)
pos[v] = arr
}
remove := func(v, idx int) {
arr := pos[v]
i := sort.Search(len(arr), func(i int) bool { return arr[i] >= idx })
if i < len(arr) && arr[i] == idx {
arr = append(arr[:i], arr[i+1:]...)
}
pos[v] = arr
}
getRange := func(v int) (int, int, bool) {
arr := pos[v]
if len(arr) == 0 {
return 0, 0, false
}
return arr[0], arr[len(arr)-1], true
}
seg := newSegTree(n)
distinct := 0
for _, v := range nums {
if isPrime[v] && len(pos[v]) == 0 {
distinct++
}
insert(v, 0)
}
for v := range pos {
if isPrime[v] && len(pos[v]) > 0 {
l, r, _ := getRange(v)
seg.add(l+1, r, 1)
}
}
res := make([]int, 0, len(queries))
for _, q := range queries {
idx, val := q[0], q[1]
old := nums[idx]
if old == val {
res = append(res, distinct+seg.queryMax())
continue
}
if isPrime[old] {
l, r, ok := getRange(old)
if ok {
seg.add(l+1, r, -1)
}
}
remove(old, idx)
nums[idx] = val
insert(val, idx)
if isPrime[val] {
l, r, ok := getRange(val)
if ok {
seg.add(l+1, r, 1)
}
}
res = append(res, distinct+seg.queryMax())
}
return res
}
Go-specific notes
The Go version uses slices and binary search for maintaining sorted index lists per value. Unlike Python’s bisect with lists, Go requires manual insertion and deletion, which is slightly more verbose. The segment tree logic remains structurally identical, with explicit max comparisons instead of Python’s built-in max function.
Worked Examples
Example 1
Initial state: nums = [2,1,3,1,2]
Prime intervals:
- 2 → [0,4]
- 3 → [2,2]
Segment contributions:
- 2 contributes across all k in [1,4]
- 3 contributes nowhere (single point)
After query [1,2]:
Updated nums: [2,2,3,1,2]
Prime intervals:
- 2 → [0,4]
- 3 → [2,2]
Distinct primes = 2
Best split occurs where interval overlap is maximized:
- k=2 or k=3 yields overlap from 2’s interval
Answer = 3
After query [3,3]:
nums: [2,2,3,3,2]
Intervals:
- 2 → [0,4]
- 3 → [2,3]
Now both primes span multiple splits.
Maximum overlap increases, producing answer 4.
Example 2
nums = [2,1,4]
After update: [1,1,4]
Prime values:
- 2 removed
- 4 is not prime
- no distinct primes remain
Thus all contributions vanish and result is 0.
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | $O((n + q)\log n)$ | Each update modifies at most two interval endpoints and performs segment tree range updates |
| Space | $O(n)$ | Storage for segment tree and position lists |
The logarithmic factor comes from segment tree updates and ordered insert/delete operations. Since each query only affects one index, the amortized work remains efficient for large inputs.
Test Cases
assert Solution().maximumCount([2,1,3,1,2], [[1,2],[3,3]]) == [3,4] # sample case
assert Solution().maximumCount([2,1,4], [[0,1]]) == [0] # no primes
assert Solution().maximumCount([2,2,2], [[0,2],[1,3]]) == [1,1] # single prime dominance
assert Solution().maximumCount([3,5,7,11], [[2,2]]) == [3] # all primes, interval shifts
assert Solution().maximumCount([1,1,1,1], [[1,2]]) == [0] # no primes initially or after
| Test | Why |
|---|---|
| sample case | validates core behavior |
| no primes | checks zero contribution handling |
| repeated primes | tests interval stability |
| all primes | checks maximum overlap behavior |
| all ones | ensures non-primes are ignored |
Edge Cases
One important edge case is when the array contains no primes at all, either initially or after updates. In this situation, all interval structures remain empty, and both the distinct count and segment tree contribution should remain zero. The implementation handles this naturally because only prime values are inserted into the tracking structures.
Another edge case occurs when a prime appears exactly once. In this case, its interval collapses to a single point, meaning it cannot contribute to any split interval. The segment tree update range becomes invalid or empty, and therefore no contribution is added.
A third edge case is when updates repeatedly overwrite the same index with different values. This stresses correctness of removal and reinsertion logic in the position tracking structure. The solution ensures correctness by always removing the old index before inserting the new value and recomputing affected interval contributions.