← Вернуться в блог

Kadane 2D и сжатие столбцов — задача «Максимальная сумма прямоугольника» РАЗБОР

  • 1600
  • kadane
  • dynamic-programming
  • prefix-sums
  • 2d-arrays
  • max-subarray

Многие алгоритмические задачи имеют красивое одномерное решение, которое потом «поднимается» в двумерное через одну и ту же идеологию: зафиксировать координаты по одной из осей, свернуть задачу в 1D, применить готовый шаблон. Это работает для prefix sums (1D → 2D через формулу включений-исключений), для DP по подсегментам (1D → 2D с парой границ) и — для алгоритма Kadane.

Эта статья разбирает классический шаблон «Kadane 2D» на задаче «Максимальная сумма прямоугольника». Алгоритм собирает несколько полезных техник в одной задаче: одномерный Kadane, сжатие двумерного массива в одномерный за счёт фиксации границ, инкрементальное поддержание сумм по столбцам без полной пересборки на каждом шаге.


Что дано

Дана прямоугольная матрица целых чисел размера N×MN \times M. Элементы могут быть положительными, нулевыми или отрицательными. Нужно найти прямоугольную подматрицу (подматрицу из подряд идущих строк и столбцов) с максимальной суммой содержащихся в ней элементов.

Подматрица не может быть пустой — нужно выбрать хотя бы одну клетку. Если все элементы матрицы отрицательные, ответом будет наименее отрицательный одиночный элемент.

Ограничения

  • 1N,M1001 \le N, M \le 100.
  • Каждый элемент по модулю не превышает 100100.
  • Время — 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}

Первая строка — два числа NN и MM. Далее NN строк по MM чисел.

Формат вывода

Одно целое число — максимальная сумма элементов прямоугольной подматрицы.

Пример 1

Вход:

2 3
5 0 9
1 2 7

Выход:

24

Подматрица — вся матрица 2×32 \times 3. Сумма 5+0+9+1+2+7=245 + 0 + 9 + 1 + 2 + 7 = 24.

Пример 2

Вход (матрица 4×54 \times 5 с отрицательными элементами):

4 5
0 -2 -7 0 -1
9 2 -6 2 0
-4 1 -4 1 0
-1 8 0 -2 1

Выход:

15

Выгодная подматрица — нижняя левая 2×22 \times 2 или 3×13 \times 1 с положительной суммой. Подбор оптимума глазами на больших матрицах с отрицательными элементами — нетривиален; алгоритм сделает это за нас.

Разберём первый пример руками

Матрица (509127)\begin{pmatrix} 5 & 0 & 9 \\ 1 & 2 & 7 \end{pmatrix}. Все элементы неотрицательные, значит выгоднее всегда брать всю матрицу — каждый дополнительный элемент только увеличивает сумму. Ответ — 2424.

Этот вырожденный случай (все элементы положительные) показывает, что одномерный Kadane здесь не работает напрямую: он оптимизирует выбор одного подсегмента в одном массиве. Двумерный аналог должен оптимизировать прямоугольник в матрице, причём «прямоугольность» — ограничение, которое не сводится к простому подсегменту.

Идея сведения сложной 2D-задачи к простой 1D — за фиксацию пары границ. Если зафиксировать верхнюю строку r1r_1 и нижнюю строку r2r_2 прямоугольника, остаётся выбрать пару столбцов c1,c2c_1, c_2. Сумма прямоугольника — это сумма элементов из строк r1..r2r_1..r_2 и столбцов c1..c2c_1..c_2. Если для каждого столбца cc заранее посчитать «вертикальную сумму» S[c]=a[r1][c]+a[r1+1][c]++a[r2][c]S[c] = a[r_1][c] + a[r_1+1][c] + \ldots + a[r_2][c], то сумма прямоугольника — это просто сумма подсегмента S[c1]+S[c1+1]++S[c2]S[c_1] + S[c_1+1] + \ldots + S[c_2] в одномерном массиве SS.

А максимальная сумма подсегмента в одномерном массиве — это и есть классический одномерный Kadane за O(M)O(M). Получается полный алгоритм: перебрать все пары строк (их O(N2)O(N^2)), для каждой пары посчитать SS и запустить Kadane.


Идея решения: Kadane 2D через сжатие столбцов

Алгоритм в одну фразу.

Перебираем все пары строк (r1,r2)(r_1, r_2) с 0r1r2<N0 \le r_1 \le r_2 < N. Для фиксированной пары сжимаем матрицу в одномерный массив SS длины MM, где S[c]S[c] — сумма элементов в столбце cc от строки r1r_1 до r2r_2 включительно. К SS применяем одномерный Kadane — он находит максимальный подсегмент за O(M)O(M). Итог — обновляем глобальный ответ.

Одномерный Kadane (база)

Для массива bb длины mm ищем максимальную сумму непустого подсегмента.

Идея: на каждом шаге ii держим лучшую сумму подсегмента, заканчивающегося в ii — назовём её 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

Сложность: O(M)O(M) времени, O(1)O(1) памяти (хранится только best и cur).

Инкрементальное сжатие — без полной пересборки SS

Для фиксированного r1r_1, когда r2r_2 растёт от r1r_1 к N1N - 1, массив SS можно поддерживать инкрементально: при переходе от r2r_2 к r2+1r_2 + 1 добавляем к S[c]S[c] значение a[r2+1][c]a[r_2 + 1][c]. Не нужно пересчитывать всю сумму с нуля.

Это сэкономит фактор NN в общей сложности: внутренний пересчёт SS на каждой паре (r1,r2)(r_1, r_2) становится O(M)O(M), а не O(NM)O(N \cdot M).

Почему это работает

Любая прямоугольная подматрица определяется ровно четырьмя границами: (r1,r2,c1,c2)(r_1, r_2, c_1, c_2). Алгоритм перебирает все возможные пары (r1,r2)(r_1, r_2) — это внешний перебор. Для каждой пары он находит оптимальные (c1,c2)(c_1, c_2) — это внутренний Kadane.

Доказательство корректности — конструктивное. Любая оптимальная подматрица имеет какие-то конкретные значения r1,r2r_1^*, r_2^*. Когда внешний перебор дойдёт до пары (r1,r2)(r_1^*, r_2^*), в массиве SS окажутся именно вертикальные суммы между r1r_1^* и r2r_2^*. 1D-Kadane найдёт оптимальный подсегмент в SS — это именно c1,c2c_1^*, c_2^* оптимальной подматрицы. Значит, на этой итерации мы пересчитаем best так, что в нём окажется истинный максимум. Алгоритм корректен.

Альтернатива через 2D префиксные суммы

Можно построить 2D-префиксные суммы за O(NM)O(N \cdot M) один раз, а потом для каждой пары (r1,r2)(r_1, r_2) извлекать вертикальную сумму S[c]S[c] за O(1)O(1) через формулу включений-исключений (та же, что в статье 2 серии «Алгоритмические основы»). Эквивалентно по сложности и чуть прозрачнее теоретически, но чуть длиннее по коду, чем инкрементальное обновление. На N=M=100N = M = 100 оба подхода работают мгновенно — выбор стилевой.


Решение: псевдокод

прочитать 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

Сложность: O(N2M)O(N^2 \cdot M) времени, O(M)O(M) памяти на массив SS. Для N=M=100N = M = 10010610^6 операций; укладывается в TL 1 секунда с большим запасом.


Код решения

Комментарии по реализации.

  • Тип для аккумуляторов. На этой задаче int достаточно (суммаNM100=106|\text{сумма}| \le N \cdot M \cdot 100 = 10^6), но в C++ держим long long для S, cur, best, ans — привычка спасает на чуть более жёстких ограничениях (aij109|a_{ij}| \le 10^9 — типичная модификация). В Python int автоматически большой.
  • Нейтральный минимум. Python — NEG_INF = float('-inf'), операторы <, > для смешанных int/float работают как ожидается. C++ — LLONG_MIN (или -(long long)1e18).
  • (long long)S[c] в max (C++) — явное приведение, чтобы перегрузка max сработала корректно при разных типах операндов. Без него GCC иногда выдаёт ошибку компиляции «no matching function for max(long long, long long&)».
  • Кеширование row = a[r2] (Python) в горячем цикле сокращает накладные расходы на индексацию двумерного списка. Без этого a[r2][c] каждый раз делает два обращения; с кешем — одно атомное row[c].
  • Быстрый ввод. Python — sys.stdin.buffer.read().split(), на 10410^4 чисел с большим запасом. C++ — ios::sync_with_stdio(false).

Проверим на всех примерах из условия

Пример 1: N=2,M=3N = 2, M = 3, матрица (509127)\begin{pmatrix} 5 & 0 & 9 \\ 1 & 2 & 7 \end{pmatrix}

Перебор пар строк:

r1=0,r2=0r_1 = 0, r_2 = 0: S=[5,0,9]S = [5, 0, 9].

  • 1D Kadane: cur = 5; затем max(0,5+0)=5\max(0, 5 + 0) = 5, best = 5; затем max(9,5+9)=14\max(9, 5 + 9) = 14, best = 14.
  • ans = 14.

r1=0,r2=1r_1 = 0, r_2 = 1: S=[5+1,0+2,9+7]=[6,2,16]S = [5 + 1, 0 + 2, 9 + 7] = [6, 2, 16].

  • 1D Kadane: cur = 6; max(2,6+2)=8\max(2, 6 + 2) = 8, best = 8; max(16,8+16)=24\max(16, 8 + 16) = 24, best = 24.
  • ans = 24.

r1=1,r2=1r_1 = 1, r_2 = 1: S=[1,2,7]S = [1, 2, 7].

  • 1D Kadane: cur = 1; max(2,1+2)=3\max(2, 1 + 2) = 3, best = 3; max(7,3+7)=10\max(7, 3 + 7) = 10, best = 10.
  • ans = 24 (не меняется).

Ответ: 24 ✓.

Пример 2: N=4,M=5N = 4, M = 5, матрица (02701926204141018021)\begin{pmatrix} 0 & -2 & -7 & 0 & -1 \\ 9 & 2 & -6 & 2 & 0 \\ -4 & 1 & -4 & 1 & 0 \\ -1 & 8 & 0 & -2 & 1 \end{pmatrix}

Перебор всех (42)+4=10\binom{4}{2} + 4 = 10 пар строк (включая «один уровень» r1=r2r_1 = r_2). Опустим внутренние Kadane-подсчёты для каждой пары и приведём ключевые наблюдения:

  • При r1=1,r2=3r_1 = 1, r_2 = 3: S=[941,2+1+8,64+0,2+12,0+0+1]=[4,11,10,1,1]S = [9 - 4 - 1, 2 + 1 + 8, -6 - 4 + 0, 2 + 1 - 2, 0 + 0 + 1] = [4, 11, -10, 1, 1]. Kadane от [4,11,10,1,1][4, 11, -10, 1, 1] — лучший подсегмент [4,11][4, 11] с суммой 1515 (продолжать после 10-10 невыгодно: 1510+1+1=7<1515 - 10 + 1 + 1 = 7 < 15).
  • Никакая другая пара строк не даёт сумму выше 15 (проверка остальных пар — скучный счёт, опустим).

Ответ: 15 ✓.


Крайние случаи

1. N=M=1N = M = 1 — одна клетка

Матрица из одного элемента. Алгоритм инициализирует S=[a0,0]S = [a_{0,0}], Kadane возвращает a0,0a_{0,0}, ans равен этому значению. Никаких особых проверок не нужно.

2. Все элементы отрицательные

Например, матрица (3125)\begin{pmatrix} -3 & -1 \\ -2 & -5 \end{pmatrix}. Здесь оптимальная подматрица — одиночная клетка с наименее отрицательным значением, 1-1.

Корректность проверяется так: 1D-Kadane на массиве из отрицательных чисел возвращает максимум массива. В нашем алгоритме при r1=0,r2=0,S=[3,1]r_1 = 0, r_2 = 0, S = [-3, -1] — Kadane даёт max(3,1)=1\max(-3, -1) = -1. Это и есть ответ.

Принципиально важно: инициализация cur = S[0], best = S[0], а не cur = 0, best = 0. Если бы мы инициализировали нулём, для всех-отрицательной матрицы алгоритм бы вернул 0 — но «пустая подматрица не разрешена» по условию.

3. Одна строка (N=1N = 1) или один столбец (M=1M = 1)

При N=1N = 1: внешний цикл выполняется только для r1=r2=0r_1 = r_2 = 0, SS совпадает со строкой матрицы, Kadane даёт максимальный подсегмент. Это обычный 1D-Kadane, спрятанный внутри алгоритма.

Аналогично для M=1M = 1: SS всегда длины 1, Kadane возвращает S[0]S[0]. Внешний цикл по парам строк просматривает все возможные «вертикальные подмассивы» одного столбца, и ans = max(s) по всем суммам.

4. Максимальные значения

N=M=100N = M = 100, все aij=100a_{ij} = 100. Сумма всей матрицы — 100100100=106100 \cdot 100 \cdot 100 = 10^6. Это укладывается в int32 (2312.11092^{31} \approx 2.1 \cdot 10^9). На задаче с aij100|a_{ij}| \le 100 переполнения нет ни в Python, ни в C++.

5. Матрица из нулей

SS всегда нули, Kadane возвращает 0. Ответ — 0.

6. Матрица с одной положительной клеткой среди отрицательных

Например, (1010105)\begin{pmatrix} -10 & -10 \\ -10 & 5 \end{pmatrix}. Все «расширения» в стороны портят сумму, оптимум — одиночная клетка 55. Корректно через cur = max(b[i], cur + b[i]): при i=1i = 1 (новая клетка 55, после cur=10\text{cur} = -10) cur = max(5, -10 + 5) = 5, что и нужно.


Типичные ошибки

  1. Инициализация cur = 0, best = 0 в Kadane. Это даёт неверный ответ для всех-отрицательной матрицы (вернётся 0 вместо наибольшего отрицательного элемента). По условию пустая подматрица не разрешена, поэтому инициализация — обязательно cur = best = b[0].
  2. Перебор только подматриц фиксированного размера. Соблазн перебрать все r1,r2,c1,c2r_1, r_2, c_1, c_2 напрямую за O(N2M2)O(N^2 \cdot M^2) работает, но при N=M=100N = M = 100 — это 10810^8 операций, на грани TL. Когда внутри ещё нужно посчитать сумму подматрицы (без префиксов — за O(NM)O(N \cdot M)), общая сложность взлетает до O(N4M2)O(N^4 \cdot M^2) и точно не проходит.
  3. Полная пересборка SS на каждой паре (r1,r2)(r_1, r_2). Если для каждой пары пересчитывать SS суммой a[r1][c]++a[r2][c]a[r_1][c] + \ldots + a[r_2][c] напрямую — это лишний фактор NN. Инкрементальное обновление S[c] += a[r2][c] при росте r2r_2 — стандартная экономия.
  4. int вместо long long в C++ на модификациях задачи. Здесь aij100|a_{ij}| \le 100, поэтому int хватает, но в более жёстких версиях (aij109|a_{ij}| \le 10^9) int переполняется.
  5. Off-by-one в инкрементальном цикле. Цикл for r2 in range(r1, N) должен включать r1r_1 — это случай «подматрица только из одной строки r1r_1». Если написать for r2 in range(r1 + 1, N), потеряется случай однострочной подматрицы и для N=1N = 1 ответ окажется -\infty.
  6. Запутанная индексация в pre-computed prefix sums. Если использовать альтернативу через 2D-префиксные суммы, легко перепутать знаки или сместить индексы на 1. На задачах типа этой инкрементальное обновление SS проще и меньше шансов ошибиться.

Анализ сложности

  • Время: O(N2M)O(N^2 \cdot M). Внешний двойной цикл по r1,r2r_1, r_2 даёт O(N2)O(N^2) итераций. На каждой — O(M)O(M) работа внутри (инкрементальное обновление SS + 1D Kadane). Для N=M=100N = M = 100: 10610^6 операций.
  • Память: O(M)O(M) — массив SS длины MM + матрица aa размером O(NM)O(N \cdot M).
  • Запас по TL: при лимите 1 секунда 10610^6 операций укладывается с большим запасом и в Python, и в C++.

Что ещё полезно потренировать

Задачи на ту же идею — Kadane (1D и 2D) и сжатие двумерного массива в одномерный. Все — с возраст-нейтральных платформ.

  • Codeforces 327A «Hungry Sequence» или похожие задачи на 1D-Kadane уровня 1100110013001300 — для закрепления базового шаблона.
  • acmp.ru, раздел «Двумерные массивы» — задачи на работу с подматрицами, 1100–1700.
  • Codeforces задачи с тегом dp + implementation уровня 1500150017001700, в которых «сведём 2D к 1D» — общая идея, не только Kadane. Например, поиск максимального квадрата из единиц в бинарной матрице.
  • Дальше — Kadane с ограничениями. Например, «максимальная сумма подматрицы, не превосходящая KK» — это уже сложнее (O(NM2logM)O(N \cdot M^2 \log M) или хитрее), типичная задача уровня 1900190022002200.

Для следующего шага — попробовать «обратную» задачу: «минимальная сумма подматрицы», или «количество подматриц с суммой ровно KK» — комбинация Kadane / prefix sums + hashmap.


Итого

  • Идея: Kadane 2D = «фиксируем пару строк → сжимаем матрицу в массив сумм по столбцам → запускаем 1D Kadane». Внешний перебор O(N2)O(N^2), внутренний — O(M)O(M). Итог O(N2M)O(N^2 \cdot M).
  • Сложность: O(N2M)O(N^2 \cdot M) времени, O(M)O(M) памяти на массив SS (плюс O(NM)O(N \cdot M) на матрицу).
  • Инкрементальное сжатие: при росте r2r_2 обновлять S[c] += a[r_2][c], не пересчитывать с нуля.
  • Ловушки: инициализация Kadane c нуля (ломает случай «всё отрицательное»), забытая итерация r2=r1r_2 = r_1 (потеря однострочных подматриц), полная пересборка SS на каждой паре (лишний фактор NN).
  • Связь с другими техниками: prefix sums по столбцам — родственная техника; 1D Kadane — фундамент; «свести 2D к 1D через перебор границ» — общий паттерн, работающий и для других задач (max area, count of submatrices).

Попробуй разобрать похожие задачи

В CodePal AI-партнёр подсказывает идею, а не ответ. Разбор в диалоге, код проверяется в браузере.