Префиксные суммы
Наименьший стабильный индекс II
Найти первый индекс, где префиксный максимум близок к суффиксному минимуму
Постановка задачи
Описание (LeetCode)
Дан целочисленный массив nums длины n и целое число k.
Для каждого индекса i определим значение нестабильности:
max(nums[0..i]) - min(nums[i..n - 1])
То есть:
max(nums[0..i])- максимальный элемент на префиксе от0доimin(nums[i..n - 1])- минимальный элемент на суффиксе отiдоn - 1
Индекс i называется стабильным, если его значение нестабильности не больше
k.
Нужно вернуть наименьший стабильный индекс. Если такого индекса нет, верните
-1.
Пример 1:
nums = [5, 0, 1, 4], k = 3
Результат:
3
Пояснение:
- Индекс
0:max([5]) = 5,min([5, 0, 1, 4]) = 0, нестабильность5 - Индекс
1:max([5, 0]) = 5,min([0, 1, 4]) = 0, нестабильность5 - Индекс
2:max([5, 0, 1]) = 5,min([1, 4]) = 1, нестабильность4 - Индекс
3:max([5, 0, 1, 4]) = 5,min([4]) = 4, нестабильность1
Первый индекс, где нестабильность не больше k = 3, равен 3.
Пример 2:
nums = [3, 2, 1], k = 1
Результат:
-1
Пояснение:
Для каждого индекса значение нестабильности равно 2, поэтому стабильного
индекса нет.
Пример 3:
nums = [0], k = 0
Результат:
0
Пояснение:
На индексе 0 значение нестабильности равно 0 - 0 = 0.
Ограничения:
1 <= nums.length <= 10⁵0 <= nums[i] <= 10⁹0 <= k <= 10⁹
Решение
Наивное решение - для каждого индекса заново искать максимум слева и минимум
справа. Для одного индекса это O(n), для всех индексов получится O(n²).
На ограничении 10⁵ такой подход не подойдет. Нам нужно один раз подготовить
информацию, которая позволит быстро проверить любой индекс.
Эта задача хорошо ложится на идею префиксных сумм в более общем виде: заранее
считаем значение для каждого префикса или суффикса, а потом используем его за
O(1).
Тут нам нужна не сумма, а два агрегата:
- префиксный максимум:
max(nums[0..i]) - суффиксный минимум:
min(nums[i..n - 1])
Идея решения:
Суффиксный минимум для каждого индекса можно посчитать заранее. Создадим массив
right, где:
right[i] = min(nums[i..n - 1])
Его удобно строить справа налево:
right[n - 1] = nums[n - 1]
right[i] = min(nums[i], right[i + 1])
После этого идем слева направо и поддерживаем переменную left:
left = max(nums[0..i])
На каждом индексе i значение нестабильности равно:
left - right[i]
Если оно не больше k, сразу возвращаем i, потому что мы идем слева направо и
ищем самый маленький подходящий индекс.
Почему достаточно одного прохода после подготовки right?
Для индекса i нам нужны только две величины:
- Лучшее значение слева, то есть максимум на префиксе.
- Лучшее значение справа, то есть минимум на суффиксе.
Суффиксный минимум мы уже знаем из массива right. Для префиксного максимума не
нужен отдельный массив, потому что при движении слева направо он обновляется
одной операцией:
left = max(left, nums[i])
В классических префиксных суммах мы храним сумму на каждом префиксе. Здесь мы используем тот же подход, но вместо суммы берем максимум и минимум. Такой прием часто называют префиксными агрегатами.
Основные шаги:
- Создаем массив
rightдлиныn. - Заполняем
right[n - 1] = nums[n - 1]. - Идем справа налево и считаем суффиксные минимумы.
- Идем слева направо:
- обновляем
left, максимум на текущем префиксе - считаем
left - right[i] - если значение не больше
k, возвращаемi
- обновляем
- Если подходящего индекса нет, возвращаем
-1.
Рассмотрим пример:
nums = [5, 0, 1, 4], k = 3
Сначала построим right:
right[3] = 4right[2] = min(1, 4) = 1right[1] = min(0, 1) = 0right[0] = min(5, 0) = 0
Получаем:
right = [0, 0, 1, 4]
Теперь идем слева направо:
-
Индекс
0, число5.left = 5right[0] = 0- нестабильность
5 - 0 = 5 5 > 3, индекс не подходит
-
Индекс
1, число0.left = max(5, 0) = 5right[1] = 0- нестабильность
5 - 0 = 5 5 > 3, индекс не подходит
-
Индекс
2, число1.left = max(5, 1) = 5right[2] = 1- нестабильность
5 - 1 = 4 4 > 3, индекс не подходит
-
Индекс
3, число4.left = max(5, 4) = 5right[3] = 4- нестабильность
5 - 4 = 1 1 <= 3, возвращаем3
Оценка сложности
Временная сложность
Мы один раз идем справа налево, чтобы построить массив суффиксных минимумов, и один раз идем слева направо, чтобы найти первый стабильный индекс.
Итоговая временная сложность: O(n).
Пространственная сложность
Мы храним массив right длины n.
Итоговая пространственная сложность: O(n).
Код решения
Приведем код решения.
func firstStableIndex(nums []int, k int) int {
n := len(nums)
right := make([]int, n)
right[n-1] = nums[n-1]
for i := n - 2; i >= 0; i-- {
right[i] = min(nums[i], right[i+1])
}
left := 0
for i, x := range nums {
left = max(left, x)
if left-right[i] <= k {
return i
}
}
return -1
}Итоги
Эта задача показывает, что паттерн префиксных сумм шире, чем просто суммы подмассивов. Иногда нам нужно заранее посчитать другой агрегат на префиксе или суффиксе.
-
Главная идея: заранее считаем
right[i], минимальный элемент на суффиксе отiдо конца массива. -
Проверка индекса: во время прохода слева направо поддерживаем
left, максимальный элемент на префиксе, и проверяемleft - right[i] <= k. -
Почему возвращаем сразу: мы идем по индексам в порядке возрастания, поэтому первый подходящий индекс и будет ответом.
-
Сложность: два линейных прохода дают
O(n)по времени иO(n)по памяти.