LeetCode 3605 - Minimum Stability Factor of Array
We are given an array nums and an integer maxC. A subarray is called stable if the greatest common divisor (HCF/GCD) of all elements in that subarray is at least 2. The stability factor of the entire array is defined as the length of the longest stable subarray.
Difficulty: 🔴 Hard
Topics: Array, Math, Binary Search, Greedy, Segment Tree, Number Theory
Solution
LeetCode 3605 - Minimum Stability Factor of Array
Problem Understanding
We are given an array nums and an integer maxC.
A subarray is called stable if the greatest common divisor (HCF/GCD) of all elements in that subarray is at least 2.
The stability factor of the entire array is defined as the length of the longest stable subarray.
We are allowed to modify at most maxC elements, and each modified element can be changed to any integer we want.
Our goal is to minimize the stability factor after performing at most maxC modifications.
The key observation is that after changing an element, we can choose a value such as 1 or a suitable prime so that it destroys any stable subarray passing through that position. Therefore, modifications act as "break points" that can invalidate many stable subarrays simultaneously.
The constraints are large:
n ≤ 100000nums[i] ≤ 10^9
This immediately rules out any solution that examines all subarrays.
Some important edge cases are:
- Arrays containing only
1s. No stable subarray exists because every GCD equals1. - Arrays where every element is even. The entire array is stable.
maxC = 0, meaning we cannot modify anything.maxC = n, meaning every position may be changed.- Stable regions that are completely disjoint, where a single modification cannot destroy both regions.
The challenge is to determine the minimum possible longest stable subarray length after strategically placing at most maxC modifications.
Approaches
Brute Force
A brute force solution would consider every possible set of modifications and compute the resulting longest stable subarray.
Even if we ignore modifications, determining all stable subarrays by checking every interval requires O(n²) intervals. Computing GCDs for each interval leads to at least O(n² log V) complexity.
Once modifications are included, the search space becomes exponential because every element can either be modified or not.
Therefore, brute force is completely infeasible.
Key Insight
Suppose we want to know whether it is possible to make the stability factor at most L.
This means that every stable subarray must have length at most L.
Equivalently:
- Any subarray of length
L + 1whose GCD is at least2must be destroyed. - Destroying such a subarray requires modifying at least one position inside it.
Thus the problem becomes:
Given all length
L+1windows with GCD ≥ 2, can we hit every such window using at mostmaxCmodified positions?
This is a classic interval stabbing problem.
The remaining challenge is efficiently identifying all windows of length L+1 whose GCD is at least 2.
Since n is large, we need fast range GCD queries. A segment tree provides:
- Range GCD query in
O(log n) - Building in
O(n)
For a fixed L:
- Find every interval
[i, i+L]whose GCD ≥ 2. - These intervals must all be stabbed.
- The minimum number of stabbing points for intervals is obtained greedily:
- Sort by right endpoint (already naturally ordered).
- Place a modification at the right endpoint of the first uncovered interval.
- Continue.
If the required number of modifications is at most maxC, then L is feasible.
Since feasibility is monotonic, binary search on L gives the answer.
Comparison of Approaches
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | Exponential | Exponential | Enumerates modifications and stable subarrays |
| Optimal | O(n log² n) | O(n) | Binary search + segment tree + greedy interval stabbing |
Algorithm Walkthrough
Step 1
Build a segment tree storing GCD values.
This allows us to query the GCD of any interval [l, r] in O(log n) time.
Step 2
Binary search the answer L.
The answer lies between:
0n
For each candidate L, we check whether it is possible to make every stable subarray have length at most L.
Step 3
For the current L, examine every window of length L+1.
The window starting at i is:
[i, i+L]
Using the segment tree, compute its GCD.
If the GCD is at least 2, then this interval must be destroyed.
Step 4
Greedily stab all bad intervals.
Maintain:
used= number of modifications chosenlastChosen= most recent modification position
Process intervals in increasing right endpoint order.
For interval [l, r]:
- If
lastChosenalready lies inside the interval, it is covered. - Otherwise place a modification at
r. - Increment
used.
This greedy strategy produces the minimum number of stabbing points.
Step 5
If used <= maxC, then L is feasible.
Otherwise it is impossible.
Step 6
Use binary search to find the smallest feasible L.
That value is the minimum possible stability factor.
Why it works
Every stable subarray of length greater than L contains a stable subarray of exactly length L+1, because the GCD of a subarray cannot increase when extending it. Therefore eliminating all stable windows of length L+1 automatically eliminates all longer stable subarrays.
Each such window becomes an interval that must contain at least one modified position. The problem therefore reduces to finding the minimum number of points needed to hit all intervals. The standard greedy interval stabbing algorithm is optimal. Since feasibility is monotonic with respect to L, binary search correctly finds the minimum achievable stability factor.
Python Solution
from typing import List
from math import gcd
class Solution:
def minStable(self, nums: List[int], maxC: int) -> int:
n = len(nums)
size = 1
while size < n:
size <<= 1
seg = [0] * (2 * size)
for i in range(n):
seg[size + i] = nums[i]
for i in range(size - 1, 0, -1):
seg[i] = gcd(seg[i * 2], seg[i * 2 + 1])
def range_gcd(left: int, right: int) -> int:
left += size
right += size
res_left = 0
res_right = 0
while left <= right:
if left & 1:
res_left = gcd(res_left, seg[left])
left += 1
if not (right & 1):
res_right = gcd(seg[right], res_right)
right -= 1
left >>= 1
right >>= 1
return gcd(res_left, res_right)
def feasible(limit: int) -> bool:
used = 0
last_chosen = -1
for start in range(n - limit):
end = start + limit
if range_gcd(start, end) < 2:
continue
if last_chosen < start:
used += 1
last_chosen = end
if used > maxC:
return False
return True
left = 0
right = n
while left < right:
mid = (left + right) // 2
if feasible(mid):
right = mid
else:
left = mid + 1
return left
Implementation Explanation
The segment tree stores GCD values for all ranges. Every internal node contains the GCD of its two children.
The range_gcd() function performs a standard iterative segment tree query and returns the GCD of any interval in O(log n) time.
The feasible(limit) function checks whether a stability factor of at most limit can be achieved. It scans every window of length limit + 1. Whenever such a window has GCD at least 2, it becomes a bad interval that must be hit by a modification.
The greedy interval stabbing algorithm is implemented using last_chosen. If the previous chosen position already lies within the current interval, that interval is covered. Otherwise a new modification is placed at the interval's right endpoint.
Finally, binary search finds the smallest feasible limit.
Go Solution
package main
func gcd(a, b int) int {
for b != 0 {
a, b = b, a%b
}
return a
}
func minStable(nums []int, maxC int) int {
n := len(nums)
size := 1
for size < n {
size <<= 1
}
seg := make([]int, 2*size)
for i := 0; i < n; i++ {
seg[size+i] = nums[i]
}
for i := size - 1; i > 0; i-- {
seg[i] = gcd(seg[i<<1], seg[i<<1|1])
}
rangeGCD := func(left, right int) int {
left += size
right += size
resLeft := 0
resRight := 0
for left <= right {
if left&1 == 1 {
resLeft = gcd(resLeft, seg[left])
left++
}
if right&1 == 0 {
resRight = gcd(seg[right], resRight)
right--
}
left >>= 1
right >>= 1
}
return gcd(resLeft, resRight)
}
feasible := func(limit int) bool {
used := 0
lastChosen := -1
for start := 0; start < n-limit; start++ {
end := start + limit
if rangeGCD(start, end) < 2 {
continue
}
if lastChosen < start {
used++
lastChosen = end
if used > maxC {
return false
}
}
}
return true
}
left, right := 0, n
for left < right {
mid := (left + right) / 2
if feasible(mid) {
right = mid
} else {
left = mid + 1
}
}
return left
}
Go Specific Notes
The Go implementation follows the same algorithm as the Python version.
The segment tree is implemented using slices. All arithmetic safely fits inside Go's int type because values are at most 10^9, and GCD computations never exceed the original values. No special overflow handling is required.
Worked Examples
Example 1
Input:
nums = [3,5,10]
maxC = 1
Binary search eventually checks L = 1.
Windows of length 2:
| Window | GCD |
|---|---|
| [3,5] | 1 |
| [5,10] | 5 |
Bad interval:
| Interval |
|---|
| [1,2] |
Greedy stabbing:
| Interval | Action | Chosen |
|---|---|---|
| [1,2] | choose 2 | {2} |
Only one modification is needed.
Therefore L = 1 is feasible.
Answer:
1
Example 2
Input:
nums = [2,6,8]
maxC = 2
Check L = 1.
Windows of length 2:
| Window | GCD |
|---|---|
| [2,6] | 2 |
| [6,8] | 2 |
Bad intervals:
| Interval |
|---|
| [0,1] |
| [1,2] |
Greedy:
| Interval | Action | Chosen |
|---|---|---|
| [0,1] | choose 1 | {1} |
| [1,2] | already covered | {1} |
Only one modification is required.
Answer:
1
Example 3
Input:
nums = [2,4,9,6]
maxC = 1
Check L = 1.
Windows of length 2:
| Window | GCD |
|---|---|
| [2,4] | 2 |
| [4,9] | 1 |
| [9,6] | 3 |
Bad intervals:
| Interval |
|---|
| [0,1] |
| [2,3] |
Greedy:
| Interval | Action |
|---|---|
| [0,1] | choose 1 |
| [2,3] | choose 3 |
Two modifications are required.
Since maxC = 1, L = 1 is impossible.
Therefore the answer becomes:
2
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n log² n) | Binary search over answer, each feasibility check performs O(n) GCD queries |
| Space | O(n) | Segment tree storage |
The binary search performs O(log n) iterations. During each iteration we scan all windows and execute a segment tree GCD query in O(log n) time. Therefore the total complexity is O(n log² n). The segment tree requires O(n) memory.
Test Cases
sol = Solution()
assert sol.minStable([3, 5, 10], 1) == 1 # example 1
assert sol.minStable([2, 6, 8], 2) == 1 # example 2
assert sol.minStable([2, 4, 9, 6], 1) == 2 # example 3
assert sol.minStable([1], 0) == 0 # single unstable element
assert sol.minStable([2], 0) == 1 # single stable element
assert sol.minStable([1, 1, 1], 0) == 0 # no stable subarray exists
assert sol.minStable([2, 2, 2], 0) == 3 # entire array stable
assert sol.minStable([2, 2, 2], 1) == 1 # one modification breaks all
assert sol.minStable([2, 3, 5, 7], 0) == 1 # only singleton stable subarrays
assert sol.minStable([6, 12, 18, 24], 2) == 1 # heavily stable array
assert sol.minStable([2, 4, 3, 9], 0) == 2 # isolated stable regions
assert sol.minStable([1, 2, 1, 2, 1], 5) == 0 # modify all stable singletons
assert sol.minStable([1000000000], 1) == 0 # modify single element away
Test Summary
| Test | Why |
|---|---|
[3,5,10],1 |
Official example |
[2,6,8],2 |
Official example |
[2,4,9,6],1 |
Official example |
[1],0 |
No stable subarray |
[2],0 |
Single stable element |
[1,1,1],0 |
Entirely unstable array |
[2,2,2],0 |
Maximum stable segment |
[2,2,2],1 |
One modification destroys long segment |
[2,3,5,7],0 |
Only singleton stable subarrays |
[6,12,18,24],2 |
Large common divisor everywhere |
[2,4,3,9],0 |
Multiple disconnected regions |
[1,2,1,2,1],5 |
Enough modifications to eliminate all stability |
[10^9],1 |
Large value boundary |
Edge Cases
Arrays Containing Only Ones
Every subarray has GCD equal to 1, so no stable subarray exists. The correct answer is 0. A common bug is assuming every single element forms a stable subarray. The implementation correctly requires GCD at least 2.
Single Element Arrays
For nums = [x], the answer is 1 when x ≥ 2 and 0 when x = 1. The binary search and feasibility check naturally handle this because windows of length one are considered when L = 0.
Completely Stable Arrays
Consider [2,2,2,2,2]. Every subarray is stable. A naive strategy may underestimate how many intervals one modification can destroy. The greedy interval stabbing procedure correctly exploits overlap and always uses the minimum number of modifications.
Multiple Disconnected Stable Regions
For an array such as [2,4,9,6], the stable regions [2,4] and [9,6] are separated. One modification cannot destroy both regions. The interval stabbing formulation captures this exactly because the intervals do not overlap, forcing multiple modification points.
Large Values Near the Constraint Limit
Values can be as large as 10^9. Since the algorithm only performs GCD computations, it never depends on the magnitude of the numbers beyond logarithmic arithmetic inside the GCD operation. Thus large values are handled safely and efficiently.