Разбираем алгоритм Кадана: как найти максимальную сумму подмассива за O(n) на Python. Практический пример и советы для собеседований.
Когда я впервые столкнулся с задачей «найти максимальную сумму непрерывного подмассива», мой мозг лихорадочно искал решение: перебрать все возможные начала и концы? Это O(n²) — как пытаться победить босса, просто зажимая кнопки: бессмысленно и с гарантированным тайм-аутом. Я потратил час, исписывая листы вложенными циклами, тесты падали один за другим, и знакомое чувство безысходности нарастало с каждой секундой таймера.
Но суть задачи не в грубой силе, а в скрытой закономерности. Как только её видишь, решение щёлкает, будто зажигается световой меч в тёмном коридоре. Сегодня я поделюсь с вами инсайтом из динамического программирования (ДП), который превращает, казалось бы, экспоненциальный перебор в элегантный проход за O(n). Этот алгоритм — алгоритм Кадана (Kadane’s algorithm) — обязателен к изучению для каждого, кто готовится к собеседованиям или хочет прокачать навыки решения задач.
Динамическое программирование применяется, когда задача обладает двумя свойствами: оптимальной подструктурой и перекрывающимися подзадачами. Для максимальной суммы подмассива оптимальная подструктура проста: лучший подмассив, заканчивающийся на позиции i, либо состоит только из nums[i] (мы начинаем заново), либо продолжает лучший подмассив, заканчивающийся на i-1, добавляя nums[i].
Если мы знаем ответ для i-1, то можем вычислить ответ для i за константное время. Не нужно пересматривать предыдущие решения — достаточно хранить текущую «накопительную» сумму.
Рекуррентное соотношение — сердце алгоритма Кадана:
max_ending_here = max(nums[i], max_ending_here + nums[i])
max_so_far = max(max_so_far, max_ending_here)Почему это работает? Представьте, что вы идёте по тропе, собирая самоцветы (положительные числа) и иногда попадая в ямы (отрицательные числа). На каждом шаге вы спрашиваете: «Оставить ли мешок с камнями, который я несу, или выбросить его и начать новый здесь?» Если текущий камень делает мешок тяжелее, чем если начать заново, вы продолжаете нести; иначе — бросаете старый мешок и начинаете новый. Самый тяжёлый мешок, который вы когда-либо видели, и есть ответ.
Это красивый пример ДП: мы не храним таблицу всех сумм подмассивов, а лишь две переменные, которые резюмируют всю необходимую информацию о пройденном префиксе.
def max_subarray_bruteforce(nums):
best = float('-inf')
for i in range(len(nums)):
current = 0
for j in range(i, len(nums)):
current += nums[j] # сумма nums[i..j]
if current > best:
best = current
return bestДва вложенных цикла → O(n²) по времени и O(1) по памяти. Для массива из 10⁵ элементов (часто встречается на собеседованиях) это время ожидания сравнимо с попыткой эвока убежать от звёздного разрушителя.
def max_subarray_kadane(nums):
# Обрабатываем случай всех отрицательных чисел, инициализируя первым элементом
max_ending_here = max_so_far = nums[0]
for x in nums[1:]:
# Либо продолжаем предыдущий подмассив, либо начинаем заново с x
max_ending_here = max(x, max_ending_here + x)
# Сохраняем лучший результат, который видели
max_so_far = max(max_so_far, max_ending_here)
return max_so_farПочему это O(n)? Один проход по списку, константная работа на каждом элементе. Память — O(1), всего две скалярные переменные.
nums[0] (или используйте -inf и обновляйте внутри цикла).max_ending_here = max(0, max_ending_here + x): это сбрасывает сумму на ноль при отрицательных числах, ломая случай всех отрицательных. Используйте форму max(x, ...), если только задача явно не разрешает пустой подмассив.Прямое применение алгоритма Кадана. Интервьюер ожидает, что вы выведете рекуррентное соотношение и напишете чистый код.
Преобразуйте цены в разницу дневных изменений: profit[i] = price[i] - price[i-1]. Максимальная прибыль — это максимальная сумма подмассива этого массива разниц. Снова алгоритм Кадана, но в маскировке.
Обе задачи на первый взгляд выглядят по-разному, но лежащее в основе ДП идентично. Умение замечать такие сходства — признак сильного алгоритмического мышления, которое ценят интервьюеры.
Освоение алгоритма Кадана развивает навыки, которые пригодятся в реальной разработке:
Когда вы смотрите на задачу и шепчете: «Эй, это же просто бегущая сумма с опцией сброса», — вы переходите из категории «кодер» в категорию «алгоритмический волшебник». Это тот уровень понимания, который открывает новые возможности, как получение новой способности в RPG — следующий босс кажется уже не таким страшным.
Возьмите лист бумаги (или откройте любимую IDE) и попробуйте решить такую задачу:
Дан массив целых чисел. Найдите длину самого длинного подмассива, сумма которого неотрицательна.
Подсказка: преобразуйте задачу к запросу максимальной суммы подмассива на преобразованном массиве, затем примените алгоритм Кадана.
Напишите своё решение в комментариях, поделитесь озарениями или спросите, где застряли. Продолжим наше приключение — впереди ещё много драконов, и алгоритм Кадана — меч, который поможет их победить. Удачного кодинга! 🚀
Хочешь закрепить знания на практике?
Решай задачи на Algolit — интерактивная платформа для обучения
Начать бесплатно →