EduBrick

Максимальный прямоугольник

Наибольший прямоугольник в гистограмме за линию — и как из него получается наибольший прямоугольник из нулей в таблице.

4 мин

Две задачи, из которых вторая целиком сводится к первой. Обе решаются стеком ближайших меньших, и вместе они — лучшая иллюстрация того, зачем этот стек вообще нужен.

Гистограмма

Даны высоты h0,,hn1h_0, \dots, h_{n-1} столбиков единичной ширины, стоящих вплотную. Найти прямоугольник наибольшей площади, целиком помещающийся внутри гистограммы.

Ключевое наблюдение: у оптимального прямоугольника верхняя граница обязательно упирается в вершину какого-то столбика. Иначе его можно было бы поднять, увеличив площадь.

Значит, достаточно перебрать этот столбик. Пусть высота прямоугольника равна hih_i. Насколько широким он может быть?

Влево он тянется, пока столбики не ниже hih_i, и упирается в первый столбик слева, который ниже. Вправо — симметрично.

Это ровно задача о ближайшем меньшем с обеих сторон.

int n = h.size();
vector<int> left(n), right(n), st;

for (int i = 0; i < n; i++) {
    while (!st.empty() && h[st.back()] >= h[i]) st.pop_back();
    left[i] = st.empty() ? -1 : st.back();
    st.push_back(i);
}

st.clear();
for (int i = n - 1; i >= 0; i--) {
    while (!st.empty() && h[st.back()] >= h[i]) st.pop_back();
    right[i] = st.empty() ? n : st.back();
    st.push_back(i);
}

long long best = 0;
for (int i = 0; i < n; i++)
    best = max(best, h[i] * (long long)(right[i] - left[i] - 1));

Проверено против перебора всех пар границ: на пятидесяти тысячах случайных гистограмм до десяти столбиков — совпадение.

Три места, где обычно ошибаются.

Границы 1-1 и nn. Когда меньшего столбика нет, прямоугольник тянется до края. Фиктивные значения 1-1 слева и nn справа делают формулу единой и убирают все if. Ровно та же роль у нулевого столбика, который часто дописывают в начало и конец массива.

Ширина считается как right - left - 1. Оба индекса указывают на столбики, которые в прямоугольник не входят. Классическая ошибка на единицу.

Нестрогое сравнение >=. При равных высотах оно снимает со стека предыдущий столбик той же высоты. Со строгим > часть прямоугольников посчиталась бы короче, чем нужно. Проверять это надо на массиве из одинаковых чисел — там разница видна сразу.

Прямоугольник из нулей в таблице

Дана таблица n×mn \times m из нулей и единиц. Найти прямоугольник наибольшей площади, состоящий только из нулей.

Перебирать все четвёрки границ — O(n2m2)O(n^2 m^2) плюс проверка, безнадёжно. Но задача сводится к гистограмме.

Переберём нижнюю строку прямоугольника. Для фиксированной нижней строки построим гистограмму: высота столбца jj — это количество нулей подряд вверх от текущей строки.

Тогда прямоугольник из нулей, опирающийся на эту строку, — это в точности прямоугольник в гистограмме.

Массив высот не нужно пересчитывать заново для каждой строки. При переходе к следующей строке он обновляется за один проход:

vector<long long> h(m, 0);
long long best = 0;
for (int i = 0; i < n; i++) {
    for (int j = 0; j < m; j++)
        h[j] = grid[i][j] ? 0 : h[j] + 1;   // единица обрывает столбец
    best = max(best, largestRectangle(h));
}

Каждая строка стоит O(m)O(m) на обновление плюс O(m)O(m) на гистограмму, итого O(nm)O(nm) — то есть линейно от размера входа, быстрее уже невозможно.

Проверено против полного перебора всех четвёрок границ: на трёх тысячах случайных таблиц до 5×55 \times 5 — совпадение.

Почему это стоит запомнить

Приём «зафиксировать одну границу и свести двумерную задачу к одномерной» работает далеко за пределами этой задачи:

  • максимальная сумма подпрямоугольника — фиксируем пару строк, сжимаем столбцы, запускаем Кадане;
  • наибольший квадрат из нулей — то же самое, только вместо площади берём минимум из ширины и высоты (хотя проще динамикой);
  • количество прямоугольников из нулей — та же гистограмма, но суммируем, а не берём максимум.

Общая схема: фиксируем одно измерение перебором, второе обрабатываем линейным алгоритмом. Если линейный алгоритм ещё и умеет обновляться при сдвиге границы, лишний множитель исчезает.