Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions problems/3904-smallest-stable-index-ii/analysis.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# 3904. Smallest Stable Index II

[LeetCode Link](https://leetcode.com/problems/smallest-stable-index-ii/)

Difficulty: Medium
Topics: Array, Prefix Sum
Acceptance Rate: 75.2%

## Hints

### Hint 1

The brute-force reading of the problem is "for each index `i`, scan left for a max and scan right for a min," which is O(n²). But notice that as `i` moves from left to right, the left window only ever *grows* and the right window only ever *shrinks*. Whenever a quantity over a growing or shrinking window is asked for at *every* index, think about precomputing it in one sweep rather than recomputing it from scratch.

### Hint 2

This is the prefix/suffix aggregate pattern (the same family as prefix sums, just with `max`/`min` instead of `+`). Define two arrays:

- `prefMax[i] = max(nums[0..i])`
- `sufMin[i] = min(nums[i..n-1])`

Each of these can be built in a single pass, because `prefMax[i]` depends only on `prefMax[i-1]` and `nums[i]`, and `sufMin[i]` depends only on `sufMin[i+1]` and `nums[i]`. Once you have both, the instability score at `i` is just one subtraction.

### Hint 3

The critical realization is that you should **not** try to binary search the answer. Both `prefMax` and `sufMin` are non-decreasing, so their difference is *not* monotonic — a stable index can be followed by an unstable one. For example, `nums = [2, 1, 3]` gives scores `1, 1, 0`, while `nums = [1, 3, 2]` gives scores `0, 1, 1`. So the predicate "index `i` is stable" has no monotone structure to exploit, and you must scan left to right and return the first index that satisfies it.

The other half of the insight is a space trick: since you walk left to right, `prefMax` never needs to be stored as an array — a single running variable suffices. Only `sufMin` has to be materialized, because it is built in the opposite direction.

## Approach

The whole problem reduces to evaluating `prefMax[i] - sufMin[i] <= k` for every `i` in increasing order and returning the first `i` that passes.

**Step 1 — build the suffix minimum array.** Walk from the right end to the left:

```
sufMin[n-1] = nums[n-1]
sufMin[i] = min(nums[i], sufMin[i+1]) for i from n-2 down to 0
```

After this pass, `sufMin[i]` holds the smallest value in `nums[i..n-1]`. This costs O(n) time and O(n) space.

**Step 2 — sweep left to right with a running prefix maximum.** Maintain a variable `prefMax` initialized to `nums[0]`; before testing index `i`, fold `nums[i]` into it:

```
prefMax = max(prefMax, nums[i])
if prefMax - sufMin[i] <= k:
return i
```

The first index that satisfies the condition is by construction the smallest stable index. If the loop finishes without a hit, return `-1`.

**Worked example** on `nums = [5, 0, 1, 4]`, `k = 3`:

| i | nums[i] | prefMax | sufMin[i] | score | stable? |
|---|---------|---------|-----------|-------|---------|
| 0 | 5 | 5 | 0 | 5 | no |
| 1 | 0 | 5 | 0 | 5 | no |
| 2 | 1 | 5 | 1 | 4 | no |
| 3 | 4 | 5 | 4 | 1 | **yes** |

We return `3`, matching the expected output. Note how index 3 becomes stable only at the very end — a good reminder of why the scan cannot stop early or binary search.

For `nums = [3, 2, 1]`, `k = 1`, the scores are `2, 2, 2`; nothing is `<= 1`, so the answer is `-1`.

Two details worth calling out. First, both endpoints are *inclusive* and the two ranges *overlap* at `i` itself, so `nums[i]` participates in both the max and the min. That guarantees the score is always `>= 0`, since `prefMax[i] >= nums[i] >= sufMin[i]`. Second, this means `i = n-1` has score `max(nums) - nums[n-1]` and `i = 0` has score `nums[0] - min(nums)` — no index is special-cased.

This problem is genuinely on the easier side for a Medium (hence the ~75% acceptance rate), but it is a clean, honest exercise in the prefix/suffix precomputation pattern, and the "don't binary search this" trap is a real one worth internalizing.

## Complexity Analysis

Time Complexity: O(n) — one right-to-left pass to build `sufMin`, plus at most one left-to-right pass.
Space Complexity: O(n) — for the `sufMin` array. The prefix maximum needs only O(1) extra space because it is consumed in the same direction it is produced.

## Edge Cases

- **Single element (`n == 1`).** The score is `nums[0] - nums[0] = 0`, which is `<= k` for any `k >= 0`, so the answer is always `0`. The suffix-min base case must be initialized from `nums[n-1]` rather than from a sentinel to make this fall out naturally.
- **No stable index at all.** A strictly decreasing array such as `[3, 2, 1]` with small `k` yields a constant score of `max - min` at every index. The function must return `-1` rather than defaulting to `0` or `n-1`.
- **`k = 0`.** Requires an exact match `prefMax[i] == sufMin[i]`. A non-decreasing array like `[1, 2, 3, 4]` satisfies this at every index (answer `0`), whereas `[2, 1]` satisfies it nowhere.
- **All elements equal.** Every score is `0`, so the answer is `0` for any valid `k`. A useful sanity check that the two windows overlapping at `i` is handled correctly.
- **Large values.** With `nums[i]` up to `10^9`, the difference fits comfortably in `int32`, but Go's `int` is 64-bit on all target platforms, so no overflow concern arises. The subtraction never goes negative, as argued above.
- **Answer only at the last index.** As in Example 1, a large leading element can keep every early index unstable. Make sure the loop actually runs through `i = n-1` and does not stop one short.
- **Non-monotone stability.** Inputs like `[1, 3, 2]` (scores `0, 1, 1`) prove a stable index can be followed by an unstable one; do not try to short-circuit the scan based on an assumed trend.
84 changes: 84 additions & 0 deletions problems/3904-smallest-stable-index-ii/problem.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
---
number: "3904"
frontend_id: "3904"
title: "Smallest Stable Index II"
slug: "smallest-stable-index-ii"
difficulty: "Medium"
topics:
- "Array"
- "Prefix Sum"
acceptance_rate: 7517.7
is_premium: false
created_at: "2026-09-05T04:45:47.067127+00:00"
fetched_at: "2026-09-05T04:45:47.067127+00:00"
link: "https://leetcode.com/problems/smallest-stable-index-ii/"
date: "2026-09-05"
---

# 3904. Smallest Stable Index II

You are given an integer array `nums` of length `n` and an integer `k`.

For each index `i`, define its **instability score** as `max(nums[0..i]) - min(nums[i..n - 1])`.

In other words:

* `max(nums[0..i])` is the **largest** value among the elements from index 0 to index `i`.
* `min(nums[i..n - 1])` is the **smallest** value among the elements from index `i` to index `n - 1`.



An index `i` is called **stable** if its instability score is **less than or equal to** `k`.

Return the **smallest** stable index. If no such index exists, return -1.



**Example 1:**

**Input:** nums = [5,0,1,4], k = 3

**Output:** 3

**Explanation:**

* At index 0: The maximum in `[5]` is 5, and the minimum in `[5, 0, 1, 4]` is 0, so the instability score is `5 - 0 = 5`.
* At index 1: The maximum in `[5, 0]` is 5, and the minimum in `[0, 1, 4]` is 0, so the instability score is `5 - 0 = 5`.
* At index 2: The maximum in `[5, 0, 1]` is 5, and the minimum in `[1, 4]` is 1, so the instability score is `5 - 1 = 4`.
* At index 3: The maximum in `[5, 0, 1, 4]` is 5, and the minimum in `[4]` is 4, so the instability score is `5 - 4 = 1`.
* This is the first index with an instability score less than or equal to `k = 3`. Thus, the answer is 3.



**Example 2:**

**Input:** nums = [3,2,1], k = 1

**Output:** -1

**Explanation:**

* At index 0, the instability score is `3 - 1 = 2`.
* At index 1, the instability score is `3 - 1 = 2`.
* At index 2, the instability score is `3 - 1 = 2`.
* None of these values is less than or equal to `k = 1`, so the answer is -1.



**Example 3:**

**Input:** nums = [0], k = 0

**Output:** 0

**Explanation:**

At index 0, the instability score is `0 - 0 = 0`, which is less than or equal to `k = 0`. Therefore, the answer is 0.



**Constraints:**

* `1 <= nums.length <= 105`
* `0 <= nums[i] <= 109`
* `0 <= k <= 109`
37 changes: 37 additions & 0 deletions problems/3904-smallest-stable-index-ii/solution.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package main

// 3904. Smallest Stable Index II
//
// The instability score at index i is max(nums[0..i]) - min(nums[i..n-1]).
// Both windows change by one element per step, so instead of recomputing them
// we precompute a suffix-minimum array in one right-to-left pass, then sweep
// left to right carrying a running prefix maximum and return the first index
// whose score is <= k.
//
// Note that prefMax and sufMin are both non-decreasing, so their difference is
// not monotonic (e.g. [1,3,2] gives scores 0,1,1) and binary search does not
// apply -- the linear scan is required.
//
// Time: O(n), Space: O(n).
func smallestStableIndex(nums []int, k int) int {
n := len(nums)
if n == 0 {
return -1
}

// sufMin[i] = min(nums[i..n-1])
sufMin := make([]int, n)
sufMin[n-1] = nums[n-1]
for i := n - 2; i >= 0; i-- {
sufMin[i] = min(nums[i], sufMin[i+1])
}

prefMax := nums[0]
for i := 0; i < n; i++ {
prefMax = max(prefMax, nums[i])
if prefMax-sufMin[i] <= k {
return i
}
}
return -1
}
77 changes: 77 additions & 0 deletions problems/3904-smallest-stable-index-ii/solution_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package main

import "testing"

func TestSmallestStableIndex(t *testing.T) {
tests := []struct {
name string
nums []int
k int
expected int
}{
{"example 1: answer only at the last index", []int{5, 0, 1, 4}, 3, 3},
{"example 2: strictly decreasing, no stable index", []int{3, 2, 1}, 1, -1},
{"example 3: single element with k = 0", []int{0}, 0, 0},
{"edge case: single large element", []int{1000000000}, 0, 0},
{"edge case: all elements equal", []int{7, 7, 7}, 0, 0},
{"edge case: non-decreasing array is stable everywhere", []int{1, 2, 3, 4}, 0, 0},
{"edge case: k = 0 never satisfied", []int{2, 1}, 0, -1},
{"edge case: k large enough for index 0", []int{10, 1, 5}, 100, 0},
{"edge case: stable index followed by unstable ones", []int{1, 3, 2}, 0, 0},
{"edge case: valley shape stable at index 0", []int{2, 1, 3}, 1, 0},
{"edge case: max value range just below k", []int{1000000000, 0}, 999999999, -1},
{"edge case: max value range exactly k", []int{1000000000, 0}, 1000000000, 0},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := smallestStableIndex(tt.nums, tt.k)
if got != tt.expected {
t.Errorf("smallestStableIndex(%v, %d) = %v, want %v", tt.nums, tt.k, got, tt.expected)
}
})
}
}

// bruteForceSmallestStableIndex is the O(n^2) definition-following reference
// implementation, used to cross-check the optimized solution.
func bruteForceSmallestStableIndex(nums []int, k int) int {
for i := range nums {
maxLeft := nums[0]
for j := 0; j <= i; j++ {
maxLeft = max(maxLeft, nums[j])
}
minRight := nums[i]
for j := i; j < len(nums); j++ {
minRight = min(minRight, nums[j])
}
if maxLeft-minRight <= k {
return i
}
}
return -1
}

func TestSmallestStableIndexAgainstBruteForce(t *testing.T) {
inputs := [][]int{
{5, 0, 1, 4},
{3, 2, 1},
{0},
{1, 3, 2},
{2, 1, 3},
{4, 4, 0, 9, 1, 9},
{9, 8, 7, 6, 5, 4, 3, 2, 1, 0},
{0, 1, 2, 3, 4, 5},
{6, 2, 6, 2, 6, 2},
}

for _, nums := range inputs {
for k := 0; k <= 10; k++ {
want := bruteForceSmallestStableIndex(nums, k)
got := smallestStableIndex(nums, k)
if got != want {
t.Errorf("smallestStableIndex(%v, %d) = %d, want %d", nums, k, got, want)
}
}
}
}