Долгое время задачи на ДП ставили меня в тупик. Во время соревнования не понимаешь, как это решать, кроме полного перебора.
Во время разбора говорят: «давайте заведем массив d[i][j]
, где i
это количество ёжиков, а j
количество белочек, а в самом массиве d[i][j]
будем хранить чего-то там». Думаешь «ЩИТО? WTF, как бы я на контесте догадался, что мне надо завести двумерный массив, а не трехмерный, например? Почему индексы это ежики и белочки, а не стоимость белочек и оставшиеся у зайчика деньги?» потом говорят «понятно, что d[i][j] = d[i – 1][j] + 100500 * d[i – 1][j + 13] + 42
» Смотришь, ну вроде да, логично, но как блин к этому прийти-то? Как бы я понял, что использовать в качестве индексов? В каком порядке эти индексы перебирать? Как понять КАК рассчитывать значение d[i][j]
из таких-то предыдущих вычисленных значений?
Чтобы этот этап непонимания закончился у вас быстрее я и написал предыдущую статью. Ок, я говорил, что нужно придумать перебор, а потом отсечь повторное вычисление одинаковых веток перебора. Но ведь бывает так, что можно придумать 100500 вариантов перебора, и далеко не в каждом будут повторяющиеся шаги, запоминание которых сделает решение разумным по сложности применительно к данным в условии ограничениям. Да это так, потом вы научитесь видеть правильные подходы, но кроме этой избитой фразы что «надо решать задачи, чтобы научиться их решать» мне есть что сказать.
Вся нужная информация есть в условии, иногда на неё нужно посмотреть с обратной стороны, изменить направление перебора в какую-то другую сторону. Давным-давно я столкнулся с некоторыми сложностями при решении задачи Folding и AlexSkidanov провел для меня её разбор по ICQ. Как мне кажется, примерно с этого времени я начал решать ДП лучше и чаще. Давайте разберем её.
Условие вкратце: есть строка из заглавных латинских букв, и правила по которым мы можем её сжимать, нужно сжать строку так, чтобы её сжатая версия была как можно короче. Сжатие описывается как то, что есть из себя сжатая строка и во что она разжимается:
1) Стока состоящая только из букв есть сжатая стока, и разжимается она в саму себя.
2) Строка, являющаяся конкатенацией двух сжатых строк A
и B
, и разжимается она в конкатенацию строк U(A)
и U(B)
, где U(X)
– то, во что разжимается строка X
3) Строка D(X)
где D
целое число большее 1, а X
сжатая строка, разжимается в конкатенацию D
строк U(X)
Примеры U(“A2(B)”) = “ABB”
, U(“3(2(A)2(B))”) = “AABBAABBAABB”
.
Ок, давайте подумаем, что мы можем делать со строками? Мы можем либо склеивать две различные строки в одну, либо склеивать N одинаковых строк X в одну вида N(X). Или можно посмотреть на задачу с другой стороны, и наоборот, разделять строку на две произвольных подстроки, или на несколько одинаковых. Оба варианта имеют право на существование, но давайте используем второй вариант, потому как его удобно реализовать рекурсивно, и моему мозгу он как-то более понятен. Т.е. пытаемся применить рекурсивный подход. Вот дали нам строку S которую надо сжать, мы можем разбить её на две, сжать их (рекурсивно) и склеить результат, либо разбить строку на N одинаковых подстрок, сжать эту подстроку в Z и вернуть строку N(Z).
string D(int L, int R) // результат сжатия для подстроки состоящей из символов i, L <= i < R
{
if (L + 1 == R) // осталась одна буква, вернем её
return string(s + L, s + R);
if (d[L][R].size()) // мы уже считали результат для этой подстроки, вернём то что ранее насчитали
return d[L][R];
string res = string(s + L, s + R);// изначальный результат это сама подстрока
int n = R - L;
for (int i = 1; i <= n; i++) // пытаемя разбить на n / i одинаковых
if (n / i >= 2 && n % i == 0 && ok(s + L, n, i)) //
{
string zip = Str(n / i) + "(" + D(L, L + i) + ")";
if (res.size() > zip.size())
res = zip;
}
for (int i = L + 1; i < R; i++) { // пытаемся разбить на две любые
string zip = D(L, i) + D(i, R);
if (res.size() > zip.size())
res = zip;
}
d[L][R] = res;
return res;
}
Int main() {
cin >> s;
cout << D(0, s.size()) << endl;
}
В качестве результата я храню прямо строку, но достаточно на самом деле хранить только длину, и не забыть про дополнительную информацию для восстановления ответа в виде строки. Рекурсию можно развернуть в циклы, т.е. считать то же самое, перебирая подстроки в порядке возрастания длины. Но это остается читателю в качестве упражнения.
Хорошая статья. Особенно второй абзац, можно перечитывать и осознавать: "Да это же про меня!"
Кажется проще было сказать, что некоторые задачи легче решить динамически, если решить их рекурсией с запоминание, что есть ленивая динамика.
Нельзя ли сначала проверить строку на "повторяемость" z- или префикс-функцией? Как по мне, так это быстрее даже будет.
Можно, но цель автора статьи в разъяснении дп людям. Поэтому, все ок.
Мне всегда очень помогал следующий подход:
Как по мне, довольно объективный критерий понимания динамики и умения ее решать — способность решить преимущественное большинство div1 500 на ТopСoder. Некоторое время назад я понял, что для хорошего результата на таких задачах надо уметь решать динамику; еще через некоторое время я понял, что динамику решать я в принципе не умею. Абсолютно не умею:(
Если кто хочет потренироваться, аналогично решается и 1183 с Тимуса. Вообще, в таких задачах(эти, ещё нахождение наибольшего подпалиндрома, наибольшей общей подстроки и т. п.) используется динамика на подотрезках, и наша задача состоит только в том, чтобы найти переход к состоянию меньшей длины(переходим к подотрезку данного подотрезка). А дальше просто итерируем по длине подотрезка и перебираем все подотрезки с данной длиной, или же составляем аналогичное рекурсивное решение.
На украинском ресурсе E-Olimp есть целый набор задач, посвященный данного рода динамике и динамике вообще. Плюс есть довольно хорошая статья о динамике в блоге Анатолия Присяжнюка (AWPRIS), а там в свою очередь ссылки на задачи.
Спасибо, хорошая статья. Как будто про меня.