Блог пользователя goo.gl_SsAhv

Автор goo.gl_SsAhv, 12 лет назад, По-русски

Долгое время задачи на ДП ставили меня в тупик. Во время соревнования не понимаешь, как это решать, кроме полного перебора.

Во время разбора говорят: «давайте заведем массив 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;
}

В качестве результата я храню прямо строку, но достаточно на самом деле хранить только длину, и не забыть про дополнительную информацию для восстановления ответа в виде строки. Рекурсию можно развернуть в циклы, т.е. считать то же самое, перебирая подстроки в порядке возрастания длины. Но это остается читателю в качестве упражнения.

  • Проголосовать: нравится
  • +70
  • Проголосовать: не нравится

»
12 лет назад, # |
  Проголосовать: нравится 0 Проголосовать: не нравится

Хорошая статья. Особенно второй абзац, можно перечитывать и осознавать: "Да это же про меня!"

»
12 лет назад, # |
  Проголосовать: нравится 0 Проголосовать: не нравится

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

»
12 лет назад, # |
  Проголосовать: нравится -6 Проголосовать: не нравится

Нельзя ли сначала проверить строку на "повторяемость" z- или префикс-функцией? Как по мне, так это быстрее даже будет.

  • »
    »
    12 лет назад, # ^ |
      Проголосовать: нравится 0 Проголосовать: не нравится

    Можно, но цель автора статьи в разъяснении дп людям. Поэтому, все ок.

»
12 лет назад, # |
  Проголосовать: нравится +59 Проголосовать: не нравится

Во время соревнования не понимаешь, как это решать, кроме полного перебора.

Мне всегда очень помогал следующий подход:

Задача_предположительно_динамика(задача)
  Для каждого возможного набора параметров инпута
    Если их произведение не превышает TL
       Если они полностью описывают состояние задачи
          Придумать переходы
          Если ТЛит, попробовать поменять параметры с ответом
          Если все еще ТЛит, искать возможные факты в задаче
          Если все еще ТЛит, проверить возможные основания бинпоиска и прочие технические вещи
          Если все еще ТЛит, continue
          Проверить, что из этой динамики не получается кэпская жадность
          Идти кодить решение
   Идти решать другую задачу
»
12 лет назад, # |
  Проголосовать: нравится +12 Проголосовать: не нравится

Как по мне, довольно объективный критерий понимания динамики и умения ее решать — способность решить преимущественное большинство div1 500 на ТopСoder. Некоторое время назад я понял, что для хорошего результата на таких задачах надо уметь решать динамику; еще через некоторое время я понял, что динамику решать я в принципе не умею. Абсолютно не умею:(

»
12 лет назад, # |
  Проголосовать: нравится 0 Проголосовать: не нравится

Если кто хочет потренироваться, аналогично решается и 1183 с Тимуса. Вообще, в таких задачах(эти, ещё нахождение наибольшего подпалиндрома, наибольшей общей подстроки и т. п.) используется динамика на подотрезках, и наша задача состоит только в том, чтобы найти переход к состоянию меньшей длины(переходим к подотрезку данного подотрезка). А дальше просто итерируем по длине подотрезка и перебираем все подотрезки с данной длиной, или же составляем аналогичное рекурсивное решение.

  • »
    »
    12 лет назад, # ^ |
      Проголосовать: нравится +1 Проголосовать: не нравится

    На украинском ресурсе E-Olimp есть целый набор задач, посвященный данного рода динамике и динамике вообще. Плюс есть довольно хорошая статья о динамике в блоге Анатолия Присяжнюка (AWPRIS), а там в свою очередь ссылки на задачи.

»
12 лет назад, # |
Rev. 2   Проголосовать: нравится +5 Проголосовать: не нравится

Спасибо, хорошая статья. Как будто про меня.