Массивы
Максимальная абсолютная сумма подмассива
Найти подмассив с максимальной абсолютной суммой
Постановка задачи
Описание (LeetCode)
Дан целочисленный массив nums. Абсолютная сумма подмассива nums[l ... r]
это abs(nums[l] + nums[l+1] + ... + nums[r-1] + nums[r]).
Найдите максимальную абсолютную сумму любого (возможно пустого) подмассива.
Заметим, что abs(x) определяется как:
- Если
xэто отрицательное целое число, тогдаabs(x) = -x. - Если
xэто неотрицательное целое число, тогдаabs(x) = x.
Пример 1:
nums = [1, -3, 2, 3, -4]
Результат:
5
Пояснение:
Подмассив [2, 3] имеет максимальную абсолютную сумму, равную abs(2 + 3) = abs(5) = 5.
Пример 2:
nums = [2, -5, 1, -4, 3, -2]
Результат:
8
Пояснение:
Подмассив [-5, 1, -4] имеет максимальную абсолютную сумму, равную abs(-5 + 1 - 4) = abs(-8) = 8.
Ограничения:
1 <= nums.length <= 10⁵-10⁴ <= nums[i] <= 10⁴
Решение
Мы уже знакомы с классическим алгоритмом Кадана для поиска максимальной суммы подмассива из разбора: Максимальный подмассив.
Задача поиска максимальной абсолютной суммы подмассива очень похожа, с той лишь разницей, что найти нужно абсолютную сумму.
Идея решения:
В базовой задаче мы игнорировали отрицательные суммы, так как они уменьшали наш результат. Здесь же правила меняются: "большой минус" для нас так же важен, как и "большой плюс".
По сути, нам нужно найти подмассив, который максимально отклоняется от нуля в любую сторону. Другими словами, нам нужен подмассив либо с максимальной, либо с минимальной суммой.
Давайте здесь остановимся и немного подумаем, как мы могли бы расширить логику алгоритма Кадана, чтобы эффективно решить этот вариант?
Задайте себе вопрос: как бы мы решали задачу, если бы нужно было искать не максимальную, а минимальную сумму подмассива?
Самое наивное, что приходит в голову: мы могли бы поменять знак всех чисел в массиве на противоположный и использовать тот же алгоритм Кадана для поиска максимальной суммы, но теперь найденная сумма была бы минимальной для оригинального массива, так как мы изменили знаки у чисел. А это в свою очередь эквивалентно тому, что мы для текущей суммы отслеживаем не максимум, а минимум.
Это и есть ключевая идея решения - использовать алгоритм Кадана дважды для поиска максимальной суммы и для поиска минимальной суммы, а затем выбрать из них ту, у которой абсолютное значение больше.
Чтобы решить задачу за один проход, мы объединим поиск максимума и минимума внутри одного цикла и вместо замены знака у чисел будем явно отслеживать минимальную сумму.
Основные шаги:
- Создаем переменные
currentMax(для текущего максимума) иcurrentMin(для текущего минимума), а такжеabsMaxдля хранения глобального результата. Все они в начале равны0. - Идем циклом по массиву:
- Обновляем
currentMax, выбирая большее междуnums[i]иcurrentMax + nums[i](классический Кадан). - Обновляем
currentMin, выбирая меньшее междуnums[i]иcurrentMin + nums[i](зеркальный Кадан). - Обновляем
absMax, сравнивая текущийabsMaxсcurrentMaxи абсолютным значениемcurrentMin.
- Обновляем
- Возвращаем
absMax.
В классическом алгоритме Кадана текущая и результирующая сумма инициализируются первым элементом массива, чтобы корректно обработать массивы из одного элемента или массивы только из отрицательных чисел.
Здесь же мы ищем абсолютное значение. Если мы начнем с 0, это эквивалентно
допущению, что мы можем выбрать "пустой подмассив" с суммой 0. Поскольку
абсолютная сумма не может быть меньше нуля, 0 - это безопасное значение суммы.
После обсуждения идеи решения сделаем ручной прогон на примере и проверим наш подход.
Пример 1:
Как это работает на примере [1, -3, 2, 3, -4].
-
На старте:
currentMax = 0,currentMin = 0,absMax = 0. -
Далее видим
1:currentMax = max(1, 0+1) = 1currentMin = min(1, 0+1) = 1absMax = max(0, 1, 1) = 1
-
Видим
-3:currentMax = max(-3, 1-3) = -2currentMin = min(-3, 1-3) = -3absMax = max(1, -2, 3) = 3
-
Число
2:currentMax = max(2, -2+2) = 2currentMin = min(2, -3+2) = -1absMax = max(3, 2, 1) = 3
-
Число
3:currentMax = max(3, 2+3) = 5currentMin = min(3, -1+3) = 2absMax = max(3, 5, 2) = 5
-
Число
-4:currentMax = max(-4, 5-4) = 1currentMin = min(-4, 2-4) = -2absMax = max(5, 1, 2) = 5
Итог:
- Максимальная положительная сумма была достигнута на шаге
5(подмассив[2, 3]), результат5. - Минимальная отрицательная сумма
-3была на шаге3(подмассив[-3]), абсолютное значение3. - Финальный ответ:
5.
Также нужно не забыть проверить граничные случаи:
- пустой массив:
- вернем
0(цикл не сделает ни одной итерации) - верно
- вернем
- содержит один элемент:
- вернем абсолютное значение этого элемента - верно
- все числа положительные:
- вернем сумму всех чисел (классический Кадан) - верно
- все числа отрицательные:
- вернем абсолютное значение суммы всех чисел (зеркальный Кадан) - верно
Оценка сложности
Временная сложность
После того как мы обсудили решение и проверили на тестовых примерах, нужно не забыть сделать оценку сложности.
В нашем решении мы проходим по массиву ровно один раз с помощью одного цикла. Каждая итерация цикла выполняет операции с константным временем.
Таким образом, это линейная сложность: O(n).
Пространственная сложность
Мы используем фиксированное количество дополнительной памяти, которое не зависит от размера входного массива.
Для решения нам требуется только несколько переменных типа int, то есть это
константная сложность: O(1).
Это решение является оптимальным. Не получится решить задачу быстрее, чем за
O(n), так как мы обязаны посмотреть каждый элемент хотя бы один раз и нельзя
использовать меньше памяти, чем O(1), так как нам в любом случае нужно где-то
хранить результат.
Код решения
После того как мы обсудили решение, проверили на тестовых примерах, оценили временную и пространственную сложность и выяснили, что наше решение является оптимальным, можем написать код решения.
func maxAbsoluteSum(nums []int) int {
currentMax, currentMin, absMax := 0, 0, 0
for i := 0; i < len(nums); i++ {
currentMax = max(nums[i], currentMax + nums[i])
currentMin = min(nums[i], currentMin + nums[i])
absMax = max(absMax, max(currentMax, abs(currentMin)))
}
return absMax
}
Итоги
Задача поиска максимальной абсолютной суммы подмассива - это отличный пример того, как базовая концепция (алгоритм Кадана) адаптируется под более гибкие условия.
В качестве дополнительных упражнений на закрепление алгоритма Кадана мы рекомендуем вам самостоятельно решить задачи:
-
Best Time to Buy and Sell Stock (Подсказка: в качестве исходного массива рассмотрите дельты между ценами по дням)
-
Maximum Product Subarray (Подсказка: как и в текущей задаче, отслеживаем и обновляем текущее минимальное и максимальное произведение подмассива в зависимости от элемента и поддерживаем глобальный максимум)
- Двойственность задачи: максимальное абсолютное значение находится либо в подмассиве с самой большой положительной суммой, либо в подмассиве с самой маленькой отрицательной суммой. Нахождение обоих - кратчайший путь к ответу.
- Эффективность: использование модифицированного алгоритма Кадана позволяет
решить задачу за один проход (
O(n)по времени), с использованием минимума памяти. - Граничные случаи: инициализация сумм нулями упрощает код, избавляя от лишних проверок на пустые массивы или знаки чисел, так как абсолютная сумма не может быть меньше нуля.
- Оптимальное решение: сочетает простоту и эффективность благодаря алгоритму Кадана.