EduBrick

Экономия памяти

Таблица не помещается в лимит. Скользящие строки, свёртка в один массив и цена, которую за это платят.

4 мин

Время динамики — произведение числа состояний на стоимость перехода, и уменьшить его обычно нельзя, не поменяв алгоритм. А вот память уменьшается почти всегда, и делается это механически.

Считать заранее полезно: таблица 104×10410^4 \times 10^4 из int — это 400 МБ, то есть отказ по памяти при типичном лимите 256 МБ. Таблица 103×10510^3 \times 10^5 из long long — 764 МБ.

Две строки

Если переход смотрит только на предыдущую строку, вся таблица не нужна.

vector<int> prev(m + 1, 0), cur(m + 1, 0);
for (int i = 1; i <= n; i++) {
    for (int j = 1; j <= m; j++)
        cur[j] = (a[i - 1] == b[j - 1]) ? prev[j - 1] + 1
                                        : max(prev[j], cur[j - 1]);
    swap(prev, cur);
}
// ответ лежит в prev — после последнего swap

Проверено: на 20 000 досках до 8×88 \times 8 результат совпадает с полной таблицей.

Замер на НОП двух строк по 5000 символов: полная таблица int — 95 МБ, две строки — 0,04 МБ, время 45 мс.

Две ловушки. Первая: swap векторов в C++ меняет местами внутренние указатели и стоит O(1)O(1) — копирования нет. Писать prev = cur вместо этого можно, но это лишнее копирование на каждой строке.

Вторая: после цикла ответ лежит в prev, а не в cur — последний swap уже произошёл. Ошибка тихая: программа выводит значение предпоследней строки.

Одна строка

Иногда хватает и одного массива — если аккуратно выбрать направление обхода. Классика — рюкзак:

for (int i = 0; i < n; i++)
    for (int j = W; j >= w[i]; j--)
        d[j] = max(d[j], d[j - w[i]] + c[i]);

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

Поэтому свёртка в один массив возможна не всегда, а только когда переход смотрит строго в одну сторону по второму индексу. Если он смотрит и влево, и вправо — нужны две строки.

Проверьте себя вопросом: «какое значение я читаю — до обновления или после?» Если ответ «до» и это правильно — обход выбран верно.

Хранить только по модулю

Когда переход смотрит на kk строк назад, держат k+1k + 1 строку и обращаются по циклическому индексу.

vector<vector<long long>> d(3, vector<long long>(m + 1, 0));
for (int i = 0; i <= n; i++) {
    // d[i % 3] считается через d[(i - 1) % 3] и d[(i - 2) % 3]
}

Приём выручает в задачах вида «нельзя ставить одинаковое ближе чем через kk». Единственное, о чём надо помнить: перед вычислением новой строки её надо очистить — там лежат данные с шага i−k−1i - k - 1, и они не нули.

Пропущенная очистка — одна из самых неприятных ошибок в динамике: ответ получается почти правильным, потому что мусор влияет не на все состояния.

Чем платят

Восстановлением ответа. По двум строкам не проследить путь. Если условие просит сам ответ, а не его величину, есть три выхода:

  • держать таблицу целиком, если она влезает;
  • хранить не значения, а только направления переходов — обычно это два бита на состояние, в восемь раз меньше int;
  • делить пополам по Хиршбергу: посчитать половину таблицы слева, половину справа, найти точку стыка, рекурсивно разобрать обе половины. Памяти O(min⁡(n,m))O(\min(n, m)), времени вдвое больше.

Читаемостью. Свёрнутая динамика короче, но по ней не видно, что происходит. Разумный порядок работы: сначала написать честную двумерную версию, проверить её на примерах, и только потом свернуть — сравнивая ответы на тех же тестах.

Когда экономить не надо

Если таблица помещается — не сворачивайте. Свёртка стоит времени на написание, ломает восстановление ответа и вносит ошибки, которые ищутся дольше, чем экономится памяти.

Считайте до, а не после: перемножьте размеры, умножьте на размер типа, сравните с лимитом. Восемь мегабайт таблицы — это норма, сто — повод свернуть.

И держите в голове разницу между int и long long: она вдвое, и иногда именно она решает. Если ответ заведомо мал, а промежуточные значения — нет, полезно посмотреть, где на самом деле нужен большой тип.