Скользящее окно
Минимальный подмассив с суммой
Найти минимальную длину подмассива с суммой не меньше target
Постановка задачи
Описание (LeetCode - 209. Minimum Size Subarray Sum)
Дан массив положительных целых чисел nums и положительное число target.
Нужно найти минимальную длину непрерывного подмассива, сумма которого больше или
равна target.
Если такого подмассива нет, нужно вернуть 0.
Пример 1:
target = 7, nums = [2, 3, 1, 2, 4, 3]
Результат:
2
Пояснение:
Подмассив [4, 3] имеет минимальную длину среди всех подмассивов с суммой не
меньше 7.
Пример 2:
target = 4, nums = [1, 4, 4]
Результат:
1
Пример 3:
target = 11, nums = [1, 1, 1, 1, 1, 1, 1, 1]
Результат:
0
Ограничения:
1 <= target <= 10⁹1 <= nums.length <= 10⁵1 <= nums[i] <= 10⁴
Решение
Наивное решение перебирает все возможные подмассивы и считает сумму для каждого.
Даже при инкрементальном подсчете это требует O(n²), что слишком медленно для
10⁵ элементов.
Главное свойство задачи: все числа в массиве положительные. Это гарантирует монотонность суммы окна:
- при сдвиге правой границы
rightвправо сумма только увеличивается; - при сдвиге левой границы
leftвправо сумма только уменьшается.
Благодаря этому можно применить динамическое скользящее окно с двумя указателями.
Идея решения:
Поддерживаем окно [left..right] и текущую сумму его элементов windowSum.
- Двигаем
rightвправо и прибавляемnums[right]кwindowSum. - Как только
windowSum >= target, текущее окно становится подходящим:- обновляем минимальную длину:
minLength = min(minLength, right - left + 1); - сдвигаем
leftвправо и вычитаемnums[left]из суммы, проверяя, можно ли сделать подходящее окно еще короче.
- обновляем минимальную длину:
- Повторяем сужение окна, пока
windowSum >= target. Когда сумма станет меньшеtarget, снова продолжаем двигать правую границу.
Основные шаги:
- Инициализируем
left = 0,windowSum = 0иminLength = infinity. - Проходим указателем
rightпо всему массиву. - Добавляем
nums[right]вwindowSum. - Пока
windowSum >= target:- обновляем
minLength = min(minLength, right - left + 1); - вычитаем
nums[left]изwindowSum; - увеличиваем
leftна1.
- обновляем
- Если подходящее окно не было найдено (
minLengthне изменился), возвращаем0, иначе возвращаемminLength.
Рассмотрим пример target = 7, nums = [2, 3, 1, 2, 4, 3].
- Расширяем окно вправо, пока сумма не станет не меньше
target. Получаем[2, 3, 1, 2], сумма равна8, длина равна4.
- Сужаем окно слева. Убираем
2, сумма становится6 < 7. Окно перестало подходить, поэтому снова расширяем его вправо.
- Добавляем
4: окно[3, 1, 2, 4], сумма равна10.
- Сужаем окно слева. Убираем
3: окно[1, 2, 4], сумма равна7, длина равна3. Это новый минимум.
- Сумма все еще не меньше
target, сужаем дальше. Убираем1: окно[2, 4], сумма равна6.
- Добавляем
3: окно[2, 4, 3], сумма равна9.
- Сужаем до
[4, 3]. Сумма равна7, длина равна2. Это минимальная длина, при дальнейшем сужении сумма станет меньшеtarget.
Итог: ответ равен 2.
Этот подход работает только потому, что все числа в массиве строго положительные. Если бы встречались отрицательные числа, сумма при расширении окна могла бы уменьшаться, а при сужении - увеличиваться. Без монотонности скользящее окно неприменимо.
Оценка сложности
Временная сложность
Указатель right проходит по массиву один раз, а указатель left также
двигается только вправо и проходит по массиву не более одного раза. Каждый
элемент входит в окно и покидает его максимум один раз.
Итоговая временная сложность: O(n).
Пространственная сложность
Мы храним только сумму окна, границы и минимальную длину. Дополнительная память не зависит от размера входных данных.
Пространственная сложность: O(1).
Код решения
Приведем код решения.
import "math"
func minSubArrayLen(target int, nums []int) int {
left := 0
windowSum := 0
minLength := math.MaxInt32
for right := 0; right < len(nums); right++ {
windowSum += nums[right]
for windowSum >= target {
length := right - left + 1
minLength = min(minLength, length)
windowSum -= nums[left]
left++
}
}
if minLength == math.MaxInt32 {
return 0
}
return minLength
}
Итоги
Задача Minimum Size Subarray Sum - базовый пример динамического скользящего
окна, где нужно найти минимальный подходящий подмассив.
- Суть алгоритма: расширяем окно вправо, пока сумма меньше
target, а затем сужаем его слева, пока условиеwindowSum >= targetсохраняется, обновляя минимальную длину. - Эффективность:
- Временная сложность:
O(n), так как оба указателя проходят массив не более одного раза. - Пространственная сложность:
O(1), алгоритм использует фиксированное число переменных.
- Временная сложность:
- Граничные случаи:
- Подходящего подмассива нет (сумма всех элементов меньше
target): возвращаем0. - Один элемент больше или равен
target: минимальная длина равна1.
- Подходящего подмассива нет (сумма всех элементов меньше
- Оптимальное решение: скользящее окно находит ответ за один проход
O(n)благодаря монотонности суммы положительных чисел.