Редакция для Лопаем шарики
Submitting an official solution before solving the problem yourself is a bannable offence.
Автор:
Лопаем шарики — разбор задачи
1. Идея
Если пытаться лопать шарики в прямом порядке и каждый раз выбирать, какой удалить следующим, получится слишком много вариантов: все перестановки из n элементов.
Главная идея — смотреть не на первый лопнутый шарик, а на последний шарик внутри некоторого отрезка.
Это стандартный приём для задач, где после удаления элементы "схлопываются" и соседи меняются. Если выбрать последний шарик на отрезке, то в этот момент его соседи уже известны: это границы отрезка. Значит, монеты за этот последний шарик можно посчитать однозначно.
Отсюда появляется динамика по подотрезкам.
2. Наблюдения
Наблюдение 1. Удобно добавить фиктивные границы
По условию, если соседа слева или справа нет, вместо него используется 1.
Чтобы не разбирать отдельно края массива, добавим:
- слева фиктивный шарик со значением
1, - справа фиктивный шарик со значением
1.
Пусть новый массив называется b:
b[0] = 1,b[1..n] = a[1..n],b[n+1] = 1.
Теперь задача выглядит так: нужно лопнуть все настоящие шарики между 0 и n+1.
Наблюдение 2. Состояние DP
Рассмотрим dp[l][r] — максимальное число монет, которое можно получить, если лопнуть все шарики строго между индексами l и r.
При этом шарики l и r пока не лопаются и считаются границами.
То есть интервал (l, r) — это внутренность между двумя ещё живыми шариками l и r.
Наблюдение 3. Кто лопается последним?
Пусть в интервале (l, r) последним лопается шарик k, где l < k < r.
К моменту, когда мы лопаем k:
- все шарики между
lиkуже удалены, - все шарики между
kиrуже удалены, - значит, ближайшие живые соседи шарика
k— это именноlиr.
Тогда за последний шарик k мы получим:
b[l] * b[k] * b[r]
А до этого мы независимо получаем:
dp[l][k]за левую часть,dp[k][r]за правую часть.
Значит,
dp[l][r] = max(dp[l][k] + dp[k][r] + b[l] * b[k] * b[r]) по всем k между l и r.
3. Алгоритм
- Считываем
nи массивa. - Строим массив
b = [1] + a + [1]. - Создаём таблицу
dpразмера(n + 2) x (n + 2), заполненную нулями. - Перебираем длину интервала
lengthот2доn + 1.- Именно с
2, потому что если междуlиrнет шариков, тоr = l + 1, и значение уже равно0.
- Именно с
- Для каждого левого конца
lнаходимr = l + length. - Перебираем, какой шарик
kбудет последним в интервале(l, r). - Обновляем
dp[l][r]максимумом:dp[l][k] + dp[k][r] + b[l] * b[k] * b[r].
- Ответ —
dp[0][n + 1].
4. Почему это работает
Докажем корректность такого перехода.
Рассмотрим произвольный интервал (l, r) и оптимальный способ лопнуть все шарики внутри него.
Среди шариков внутри интервала какой-то шарик k будет лопнут последним.
Тогда:
- все шарики из
(l, k)были лопнуты раньше, - все шарики из
(k, r)были лопнуты раньше, - в момент удаления
kвнутри(l, r)больше никого не осталось.
Значит, ближайшие живые соседи шарика k — ровно l и r, и за него получаем b[l] * b[k] * b[r].
Кроме того, действия внутри (l, k) и (k, r) не мешают друг другу: это независимые подзадачи. Поэтому суммарная прибыль равна:
- прибыль слева,
- плюс прибыль справа,
- плюс монеты за последний шарик
k.
То есть любой оптимальный ответ для (l, r) имеет вид:
dp[l][k] + dp[k][r] + b[l] * b[k] * b[r]
для некоторого k.
С другой стороны, если выбрать любой k и оптимально решить левую и правую подзадачи, а потом лопнуть k последним, получится корректный порядок лопания для всего интервала (l, r).
Значит, достаточно перебрать все возможные k и взять максимум. Именно это и делает формула перехода.
Так как мы считаем интервалы по возрастанию длины, все меньшие подзадачи уже известны к моменту вычисления dp[l][r].
Следовательно, алгоритм правильно находит максимум.
5. Сложность
Размер таблицы dp — O(n^2).
Для каждого состояния dp[l][r] мы перебираем все k между l и r, то есть ещё O(n).
Итоговая сложность:
- по времени:
O(n^3) - по памяти:
O(n^2)
При n <= 300 это проходит.
6. Код на C++17
#include <iostream>
#include <vector>
using namespace std;
int main() {
int n;
cin >> n;
vector<long long> b(n + 2, 1);
for (int i = 1; i <= n; i++) {
cin >> b[i];
}
vector<vector<long long>> dp(n + 2, vector<long long>(n + 2, 0));
for (int len = 2; len <= n + 1; len++) {
for (int l = 0; l + len <= n + 1; l++) {
int r = l + len;
long long best = 0;
for (int k = l + 1; k < r; k++) {
long long cur = dp[l][k] + dp[k][r] + b[l] * b[k] * b[r];
if (cur > best) {
best = cur;
}
}
dp[l][r] = best;
}
}
cout << dp[0][n + 1] << '\n';
return 0;
}
Комментарии