Многие алгоритмические задачи имеют красивое одномерное решение, которое потом «поднимается» в двумерное через одну и ту же идеологию: зафиксировать координаты по одной из осей, свернуть задачу в 1D, применить готовый шаблон. Это работает для prefix sums (1D → 2D через формулу включений-исключений), для DP по подсегментам (1D → 2D с парой границ) и — для алгоритма Kadane.
Эта статья разбирает классический шаблон «Kadane 2D» на задаче «Максимальная сумма прямоугольника». Алгоритм собирает несколько полезных техник в одной задаче: одномерный Kadane, сжатие двумерного массива в одномерный за счёт фиксации границ, инкрементальное поддержание сумм по столбцам без полной пересборки на каждом шаге.
Что дано
Дана прямоугольная матрица целых чисел размера . Элементы могут быть положительными, нулевыми или отрицательными. Нужно найти прямоугольную подматрицу (подматрицу из подряд идущих строк и столбцов) с максимальной суммой содержащихся в ней элементов.
Подматрица не может быть пустой — нужно выбрать хотя бы одну клетку. Если все элементы матрицы отрицательные, ответом будет наименее отрицательный одиночный элемент.
Ограничения
- .
- Каждый элемент по модулю не превышает .
- Время — 1 секунда, память — 64 МБ.
Формат ввода
N M
a_{1,1} a_{1,2} ... a_{1,M}
a_{2,1} a_{2,2} ... a_{2,M}
...
a_{N,1} a_{N,2} ... a_{N,M}
Первая строка — два числа и . Далее строк по чисел.
Формат вывода
Одно целое число — максимальная сумма элементов прямоугольной подматрицы.
Пример 1
Вход:
2 3
5 0 9
1 2 7
Выход:
24
Подматрица — вся матрица . Сумма .
Пример 2
Вход (матрица с отрицательными элементами):
4 5
0 -2 -7 0 -1
9 2 -6 2 0
-4 1 -4 1 0
-1 8 0 -2 1
Выход:
15
Выгодная подматрица — нижняя левая или с положительной суммой. Подбор оптимума глазами на больших матрицах с отрицательными элементами — нетривиален; алгоритм сделает это за нас.
Разберём первый пример руками
Матрица . Все элементы неотрицательные, значит выгоднее всегда брать всю матрицу — каждый дополнительный элемент только увеличивает сумму. Ответ — .
Этот вырожденный случай (все элементы положительные) показывает, что одномерный Kadane здесь не работает напрямую: он оптимизирует выбор одного подсегмента в одном массиве. Двумерный аналог должен оптимизировать прямоугольник в матрице, причём «прямоугольность» — ограничение, которое не сводится к простому подсегменту.
Идея сведения сложной 2D-задачи к простой 1D — за фиксацию пары границ. Если зафиксировать верхнюю строку и нижнюю строку прямоугольника, остаётся выбрать пару столбцов . Сумма прямоугольника — это сумма элементов из строк и столбцов . Если для каждого столбца заранее посчитать «вертикальную сумму» , то сумма прямоугольника — это просто сумма подсегмента в одномерном массиве .
А максимальная сумма подсегмента в одномерном массиве — это и есть классический одномерный Kadane за . Получается полный алгоритм: перебрать все пары строк (их ), для каждой пары посчитать и запустить Kadane.
Идея решения: Kadane 2D через сжатие столбцов
Алгоритм в одну фразу.
Перебираем все пары строк с . Для фиксированной пары сжимаем матрицу в одномерный массив длины , где — сумма элементов в столбце от строки до включительно. К применяем одномерный Kadane — он находит максимальный подсегмент за . Итог — обновляем глобальный ответ.
Одномерный Kadane (база)
Для массива длины ищем максимальную сумму непустого подсегмента.
Идея: на каждом шаге держим лучшую сумму подсегмента, заканчивающегося в — назовём её cur. На каждой новой клетке у нас два варианта: либо продолжить уже накопленный подсегмент (тогда новый cur = cur + b[i]), либо сбросить его и начать заново с одной клетки (тогда cur = b[i]). Берём максимум — это выгоднее, чем тащить отрицательный префикс. Глобальный максимум best обновляем на каждом шаге.
def kadane(b: list[int]) -> int:
best = b[0]
cur = b[0]
for i in range(1, len(b)):
cur = max(b[i], cur + b[i])
best = max(best, cur)
return best
Сложность: времени, памяти (хранится только best и cur).
Инкрементальное сжатие — без полной пересборки
Для фиксированного , когда растёт от к , массив можно поддерживать инкрементально: при переходе от к добавляем к значение . Не нужно пересчитывать всю сумму с нуля.
Это сэкономит фактор в общей сложности: внутренний пересчёт на каждой паре становится , а не .
Почему это работает
Любая прямоугольная подматрица определяется ровно четырьмя границами: . Алгоритм перебирает все возможные пары — это внешний перебор. Для каждой пары он находит оптимальные — это внутренний Kadane.
Доказательство корректности — конструктивное. Любая оптимальная подматрица имеет какие-то конкретные значения . Когда внешний перебор дойдёт до пары , в массиве окажутся именно вертикальные суммы между и . 1D-Kadane найдёт оптимальный подсегмент в — это именно оптимальной подматрицы. Значит, на этой итерации мы пересчитаем best так, что в нём окажется истинный максимум. Алгоритм корректен.
Альтернатива через 2D префиксные суммы
Можно построить 2D-префиксные суммы за один раз, а потом для каждой пары извлекать вертикальную сумму за через формулу включений-исключений (та же, что в статье 2 серии «Алгоритмические основы»). Эквивалентно по сложности и чуть прозрачнее теоретически, но чуть длиннее по коду, чем инкрементальное обновление. На оба подхода работают мгновенно — выбор стилевой.
Решение: псевдокод
прочитать N, M, матрицу a[N][M]
ans ← -∞
для r1 от 0 до N - 1:
S[c] ← 0 для каждого c от 0 до M - 1
для r2 от r1 до N - 1:
для c от 0 до M - 1:
S[c] ← S[c] + a[r2][c]
# 1D Kadane на массиве S
cur ← S[0]; best ← S[0]
для c от 1 до M - 1:
cur ← max(S[c], cur + S[c])
best ← max(best, cur)
ans ← max(ans, best)
вывести ans
Сложность: времени, памяти на массив . Для — операций; укладывается в TL 1 секунда с большим запасом.
Код решения
Комментарии по реализации.
- Тип для аккумуляторов. На этой задаче
intдостаточно (), но в C++ держимlong longдляS,cur,best,ans— привычка спасает на чуть более жёстких ограничениях ( — типичная модификация). В Pythonintавтоматически большой. - Нейтральный минимум. Python —
NEG_INF = float('-inf'), операторы<,>для смешанныхint/floatработают как ожидается. C++ —LLONG_MIN(или-(long long)1e18). (long long)S[c]вmax(C++) — явное приведение, чтобы перегрузкаmaxсработала корректно при разных типах операндов. Без него GCC иногда выдаёт ошибку компиляции «no matching function formax(long long, long long&)».- Кеширование
row = a[r2](Python) в горячем цикле сокращает накладные расходы на индексацию двумерного списка. Без этогоa[r2][c]каждый раз делает два обращения; с кешем — одно атомноеrow[c]. - Быстрый ввод. Python —
sys.stdin.buffer.read().split(), на чисел с большим запасом. C++ —ios::sync_with_stdio(false).
Проверим на всех примерах из условия
Пример 1: , матрица
Перебор пар строк:
: .
- 1D Kadane: cur = 5; затем , best = 5; затем , best = 14.
- ans = 14.
: .
- 1D Kadane: cur = 6; , best = 8; , best = 24.
- ans = 24.
: .
- 1D Kadane: cur = 1; , best = 3; , best = 10.
- ans = 24 (не меняется).
Ответ: 24 ✓.
Пример 2: , матрица
Перебор всех пар строк (включая «один уровень» ). Опустим внутренние Kadane-подсчёты для каждой пары и приведём ключевые наблюдения:
- При : . Kadane от — лучший подсегмент с суммой (продолжать после невыгодно: ).
- Никакая другая пара строк не даёт сумму выше 15 (проверка остальных пар — скучный счёт, опустим).
Ответ: 15 ✓.
Крайние случаи
1. — одна клетка
Матрица из одного элемента. Алгоритм инициализирует , Kadane возвращает , ans равен этому значению. Никаких особых проверок не нужно.
2. Все элементы отрицательные
Например, матрица . Здесь оптимальная подматрица — одиночная клетка с наименее отрицательным значением, .
Корректность проверяется так: 1D-Kadane на массиве из отрицательных чисел возвращает максимум массива. В нашем алгоритме при — Kadane даёт . Это и есть ответ.
Принципиально важно: инициализация cur = S[0], best = S[0], а не cur = 0, best = 0. Если бы мы инициализировали нулём, для всех-отрицательной матрицы алгоритм бы вернул 0 — но «пустая подматрица не разрешена» по условию.
3. Одна строка () или один столбец ()
При : внешний цикл выполняется только для , совпадает со строкой матрицы, Kadane даёт максимальный подсегмент. Это обычный 1D-Kadane, спрятанный внутри алгоритма.
Аналогично для : всегда длины 1, Kadane возвращает . Внешний цикл по парам строк просматривает все возможные «вертикальные подмассивы» одного столбца, и ans = max(s) по всем суммам.
4. Максимальные значения
, все . Сумма всей матрицы — . Это укладывается в int32 (). На задаче с переполнения нет ни в Python, ни в C++.
5. Матрица из нулей
всегда нули, Kadane возвращает 0. Ответ — 0.
6. Матрица с одной положительной клеткой среди отрицательных
Например, . Все «расширения» в стороны портят сумму, оптимум — одиночная клетка . Корректно через cur = max(b[i], cur + b[i]): при (новая клетка , после ) cur = max(5, -10 + 5) = 5, что и нужно.
Типичные ошибки
- Инициализация
cur = 0, best = 0в Kadane. Это даёт неверный ответ для всех-отрицательной матрицы (вернётся 0 вместо наибольшего отрицательного элемента). По условию пустая подматрица не разрешена, поэтому инициализация — обязательноcur = best = b[0]. - Перебор только подматриц фиксированного размера. Соблазн перебрать все напрямую за работает, но при — это операций, на грани TL. Когда внутри ещё нужно посчитать сумму подматрицы (без префиксов — за ), общая сложность взлетает до и точно не проходит.
- Полная пересборка на каждой паре . Если для каждой пары пересчитывать суммой напрямую — это лишний фактор . Инкрементальное обновление
S[c] += a[r2][c]при росте — стандартная экономия. intвместоlong longв C++ на модификациях задачи. Здесь , поэтомуintхватает, но в более жёстких версиях ()intпереполняется.- Off-by-one в инкрементальном цикле. Цикл
for r2 in range(r1, N)должен включать — это случай «подматрица только из одной строки ». Если написатьfor r2 in range(r1 + 1, N), потеряется случай однострочной подматрицы и для ответ окажется . - Запутанная индексация в pre-computed prefix sums. Если использовать альтернативу через 2D-префиксные суммы, легко перепутать знаки или сместить индексы на 1. На задачах типа этой инкрементальное обновление проще и меньше шансов ошибиться.
Анализ сложности
- Время: . Внешний двойной цикл по даёт итераций. На каждой — работа внутри (инкрементальное обновление + 1D Kadane). Для : операций.
- Память: — массив длины + матрица размером .
- Запас по TL: при лимите 1 секунда операций укладывается с большим запасом и в Python, и в C++.
Что ещё полезно потренировать
Задачи на ту же идею — Kadane (1D и 2D) и сжатие двумерного массива в одномерный. Все — с возраст-нейтральных платформ.
- Codeforces 327A «Hungry Sequence» или похожие задачи на 1D-Kadane уровня – — для закрепления базового шаблона.
- acmp.ru, раздел «Двумерные массивы» — задачи на работу с подматрицами, 1100–1700.
- Codeforces задачи с тегом
dp+implementationуровня –, в которых «сведём 2D к 1D» — общая идея, не только Kadane. Например, поиск максимального квадрата из единиц в бинарной матрице. - Дальше — Kadane с ограничениями. Например, «максимальная сумма подматрицы, не превосходящая » — это уже сложнее ( или хитрее), типичная задача уровня –.
Для следующего шага — попробовать «обратную» задачу: «минимальная сумма подматрицы», или «количество подматриц с суммой ровно » — комбинация Kadane / prefix sums + hashmap.
Итого
- Идея: Kadane 2D = «фиксируем пару строк → сжимаем матрицу в массив сумм по столбцам → запускаем 1D Kadane». Внешний перебор , внутренний — . Итог .
- Сложность: времени, памяти на массив (плюс на матрицу).
- Инкрементальное сжатие: при росте обновлять
S[c] += a[r_2][c], не пересчитывать с нуля. - Ловушки: инициализация Kadane c нуля (ломает случай «всё отрицательное»), забытая итерация (потеря однострочных подматриц), полная пересборка на каждой паре (лишний фактор ).
- Связь с другими техниками: prefix sums по столбцам — родственная техника; 1D Kadane — фундамент; «свести 2D к 1D через перебор границ» — общий паттерн, работающий и для других задач (max area, count of submatrices).