Алгоритм Кадане: максимальная сумма подмассива

Алгоритм Кадане решает классическую задачу максимального подмассива (Maximum Subarray Problem): дан массив целых чисел , нужно найти непрерывный подмассив с наибольшей суммой. Джозеф Кадане (Joseph Kadane) предложил линейное решение в 1984 году, когда Джон Бентли описывал в колонке «Programming Pearls» в Communications of the ACM свой подход «разделяй и властвуй» за и поделился задачей с коллегами. Кадане за минуту в голове придумал одномерное динамическое программирование за и дополнительной памяти - оценка, которую улучшить невозможно: чтобы хотя бы один раз посмотреть на каждый элемент массива, нужно операций.
Постановка задачи и три уровня сложности
Формально: дан массив , найти пару индексов , на которой сумма максимальна. Если в массиве есть хотя бы одно положительное число, ответ положителен. Если все элементы отрицательные - ответ равен максимальному элементу (одиночному, длины 1), и это та самая ловушка, на которой реализации обычно ломаются.
Три подхода с возрастающей хитростью:
- Наивный - перебор всех пар и пересчёт суммы заново. Тривиально, на больших безнадёжно.
- Префиксные суммы - посчитать , тогда сумма на равна , перебор пар идёт за .
- Разделяй и властвуй (подход Бентли) - рекурсивно решать левую половину, правую половину и подмассив, пересекающий середину; последний считается за от центра вправо и влево.
- Алгоритм Кадане - динамическое программирование вдоль массива.
Идея динамического программирования
Введём вспомогательную величину - максимальная сумма среди всех подмассивов, заканчивающихся в позиции . Это сужение задачи: вместо всех подмассивов фиксируем правый конец и максимизируем по левому.
Заметим простое тождество. Подмассив, заканчивающийся в - это либо одиночный элемент (если выгоднее «начать заново» с него), либо продолжение лучшего подмассива, заканчивающегося в , плюс . Отсюда рекуррентность:
Это можно ещё переписать как - если накопленная сумма ушла в минус, обнуляем её перед добавлением . Глобальный ответ:
Один проход, на каждом шаге две константные операции - вот и весь алгоритм.
Псевдокод
maxEndingHere = a[1]
maxSoFar = a[1]
for i in 2..n:
maxEndingHere = max(a[i], maxEndingHere + a[i])
maxSoFar = max(maxSoFar, maxEndingHere)
return maxSoFar
Инициализация первым элементом, а не нулём - критична. Если стартовать с , то на массиве алгоритм вернёт как «пустой подмассив», а корректный ответ - . Постановка задачи требует непустого подмассива.
Для восстановления границ ведём ещё пару указателей: текущий старт start (обновляется, когда перезапускается с ) и итоговые bestL, bestR (фиксируются вместе с обновлением ).
Сложность и почему она оптимальна
Время - . Один проход по массиву, на каждой итерации две сравнения и две арифметические операции. Никаких рекурсий, никаких вспомогательных структур.
Память - дополнительной. Достаточно двух переменных и - массив хранить целиком не нужно, для DP-перехода нужен только предыдущий элемент.
Оптимальность. Любой алгоритм, выдающий правильный ответ на всех входах длины , обязан хотя бы раз прочитать каждый элемент: иначе можно изменить непрочитанный на огромное число и сломать ответ. Значит, нижняя оценка по времени - . Алгоритм Кадане эту нижнюю оценку достигает.
Если задача требует найти максимум среди подмассивов длины **не меньше $k$**, прямая модификация Кадане ломается. Используется обобщение через скользящие минимумы префиксных сумм: $\max_{r \geq k} (S_r - \min_{l \leq r-k} S_l)$.
Случай со всеми отрицательными элементами
Самая популярная ошибка реализации - версия с обнулением:
maxEndingHere = 0
maxSoFar = 0
for i in 1..n:
maxEndingHere = max(0, maxEndingHere + a[i])
maxSoFar = max(maxSoFar, maxEndingHere)
Эта версия отвечает на чуть другую задачу - «максимальная сумма подмассива, включая пустой». На массиве она вернёт , тогда как канонический Кадане - . Поведение зависит от того, какая постановка нужна заказчику. На собеседовании всегда уточняют: «допускается ли пустой подмассив?» - это не педантизм, это ровно про эту ловушку.
Безопасная универсальная форма - инициализация первым элементом и шаг . Она корректна для всех случаев, включая массив из одного отрицательного числа.
Сравнение с подходом Бентли
Подход «разделяй и властвуй»: разрезаем массив пополам, рекурсивно ищем ответ слева и справа, и отдельно - лучший подмассив, пересекающий середину. Последний считается за : от середины влево накапливаем суффиксный максимум, вправо - префиксный, и складываем.
Рекуррентность даёт по мастер-теореме . На практике медленнее Кадане в разы из-за кэш-промахов и накладных расходов на рекурсию, но это полезное упражнение на парадигму «разделяй и властвуй» - Бентли в той самой колонке использовал задачу как обучающий пример.
Современный взгляд: если асимптотически оптимален , никакой алгоритм не имеет права на жизнь в продакшене. Кадане выигрывает повсеместно.
Двумерное обобщение: максимальная подматрица
Дана матрица размером , найти прямоугольную подматрицу с максимальной суммой элементов. Прямой перебор всех подматриц - . Алгоритм Кадане обобщается до (или при правильном выборе оси).
Идея: перебираем все пары строк , где - это верхняя и нижняя границы будущей подматрицы. Для фиксированной пары строим одномерный массив - столбцовые суммы между этими строками. Лучшая подматрица с границами соответствует лучшему подмассиву массива , который мы находим обычным Кадане за .
Перебор пар - это , а столбцовые суммы можно поддерживать инкрементально (при сдвиге нижней границы обновлять за ). Итого , или на квадратной матрице. Это всё ещё лучший известный полиномиальный алгоритм - асимптотически точные нижние оценки на эту задачу неизвестны, и попытки опуститься ниже кубики связаны с гипотезой APSP и проблемой умножения матриц.
Применения
- Финансы: максимальная прибыль от одной покупки/продажи акции. Дан вектор цен . Если ввести массив разностей , то прибыль от покупки в день и продажи в день - это . Максимизация прибыли = поиск максимального подмассива в = алгоритм Кадане. LeetCode 121 «Best Time to Buy and Sell Stock».
- Биоинформатика: G+C богатые участки ДНК. Кодируем нуклеотид , если он или , и , если или . Максимальный подмассив выделяет фрагмент генома с аномально высокой долей G+C - это часто указывает на регуляторные регионы (CpG-острова) или горизонтально перенесённые гены.
- Computer vision. В задачах выделения «самого яркого» прямоугольного региона на разностном изображении (после вычитания фона или порога) применяется именно 2D Кадане - максимизировать сумму пикселей в прямоугольной области.
- Конкурсное программирование. Кадане - базовый кирпич в задачах на подсчёт подмассивов и сегментные деревья с операцией «максимальный подмассив на отрезке» (известная задача, решается через хранение четвёрки
(sum, prefMax, sufMax, ansMax)в каждом узле). В одной связке с ним часто идут линейные алгоритмы на строках - например, Z-функция строки и алгоритм Манакера для палиндромов. - Сигнальная обработка. Поиск максимального отклика на нестационарном временном ряду - Кадане выделяет интервал максимальной концентрации сигнала.
Циркулярная вариация
Если массив циркулярный (последний элемент соседствует с первым), задача распадается на два случая. Случай 1: лучший подмассив не пересекает «склейку» - это обычный Кадане. Случай 2: пересекает склейку, то есть выглядит как «кусок справа + кусок слева». Этот случай эквивалентен следующему: общая сумма массива минус минимальный подмассив (выбрасываемая середина). Алгоритм Кадане применяют дважды - для max и для min - и берут .
Тонкость: если все элементы отрицательные, формула даёт (выбросили всё), что некорректно. В этом случае ответ - обычный максимум.
Частые ошибки
- Обнуляют вместо . Так алгоритм отвечает на задачу «допускается пустой подмассив», возвращая на полностью отрицательных входах. Если по условию подмассив должен быть непустым - это баг.
- Инициализируют . Та же ловушка. Безопасно стартовать с и .
- Считают границы через post-processing. Восстановление индексов нужно делать онлайн, поддерживая
start,bestL,bestRво время основного цикла. Восстановление после факта по значениям требует ещё одного прохода и легко ошибается на дубликатах максимума. - Применяют 1D Кадане к матрице построчно. Это найдёт лучшую подматрицу высоты 1, а не настоящий 2D-максимум. Нужен перебор пар строк и сведение к 1D.
- Забывают про переполнение. На массиве из элементов до каждый сумма может достичь -
int32не подходит, используйтеint64/long.
FAQ
Чем алгоритм Кадане отличается от подхода «разделяй и властвуй»? Кадане - это динамическое программирование вдоль массива за один проход, времени и памяти. Подход Бентли - это рекурсия с разрезанием пополам и отдельным учётом подмассивов через середину, времени и памяти на стек. Кадане асимптотически и практически лучше; «разделяй и властвуй» интересен как обучающий пример парадигмы и обобщается на задачи, где DP вдоль массива не годится.
Можно ли применить Кадане, если запрещены пустые подмассивы и есть отрицательные элементы? Да, и именно это - каноническая постановка. Инициализируйте и идите со . На массиве из одних отрицательных алгоритм вернёт максимальный (наименее отрицательный) элемент, что корректно.
Как алгоритм Кадане связан с задачей покупки-продажи акции? Если перейти от цен к разностям , то прибыль от покупки в день и продажи в день - это сумма . Максимизация прибыли по всем парам - это максимальный подмассив массива разностей, который Кадане находит за . Допустимо «не торговать вообще» (прибыль ) - тогда используется версия с разрешённым пустым подмассивом и .
Коротко
Алгоритм Кадане находит максимальный подмассив за времени и дополнительной памяти через одномерное DP: , . Постановка требует непустого подмассива, поэтому критична инициализация первым элементом, иначе на полностью отрицательных входах алгоритм ошибочно возвращает . Двумерное обобщение работает через перебор пар строк и сведение к 1D за ; циркулярная версия - через . На LeetCode «Maximum Subarray» (53) и «Best Time to Buy and Sell Stock» (121), в выделении G+C-богатых участков ДНК и в подзадачах segment tree Кадане - стандартный инструмент, и его линейность невозможно улучшить асимптотически.
Читайте также

Алгоритм Карацубы: умножение длинных чисел быстрее столбика
Разбираем алгоритм Карацубы: почему три умножения вместо четырёх ускоряют перемножение длинных чисел, как выглядит рекуррентность и зачем это нужно на практике.

Алгоритм Беллмана-Форда: пути с отрицательными весами
Разбираем алгоритм Беллмана-Форда: как искать кратчайшие пути в графе с отрицательными рёбрами, ловить отрицательные циклы и чем он отличается от Дейкстры.

Алгоритм Рабина-Карпа: поиск подстроки за O(n+m)
Разбираем алгоритм Рабина-Карпа: как полиномиальный хеш и скользящее окно ускоряют поиск подстроки до O(n+m) в среднем, почему бывают ложные совпадения и при чём тут плагиат.