Методика и содержание подготовки учащихся к олимпиадам по программированию. Дистанционный курс. Динамическое программирование Несколько слов о преподавании этой темы Перед тем как приступить к изучению этой темы (или параллельно с ней) в качестве существенного подспорья можно рассказать учащимся о методе математической индукции и провести один-два семинара по решению математических задач. Также, возможно кто-то найдет для себя полезным добавить к этому материалу тему «индуктивные функции» (по Кушниренко А.Г.), некоторые задачи и теорию о которых можно найти в книге «Программирование в теоремах и задачах». Поскольку темы «Рекурсия», «Математическая индукция» и «Динамическое программирование» опираются, по сути, на одну и ту же идею то имеет смысл изучать их без больших перерывов по времени. Пара слов о том, почему рекурсивное решение не всегда хорошо При написании программ иногда может появиться проблема выбора — проводить ли оптимизацию программы по времени работы или по используемой памяти. Хранить в памяти (или во внешнем файле) некоторый информационный объект или тратить время на его создание (например, расчет числа по формуле) каждый раз, когда в нем возникает необходимость. Рассмотрим задачу о нахождении k-го числа ряда: 1, 1, 2, 3, 5, 8, 13, 21... Каждый элемент, кроме первых двух определяется как сумма двух предыдущих. В лекции о рекурсии было записано решение этой задачи с помощью рекурсивной функции: function F(k : integer): integer; begin if k<=2 then F:=1 else F:= F(k-1) + F(k-2); end; Недостаток данного решения будет очевиден, если изобразить, к примеру, такую схему: F(8) F(7) F(6) F(6) F(5) F(5) F(4) F(4) F(5) F(3) F(4) F(4) F(3) F(3) F(2) F(4) Для того, чтобы найти F(8) нужно найти F(7) и F(6). Для F(7) нужно найти F(6) и F(5). И так далее. Как видно из схемы, уже значение F(4) для разных целей должно при таком решении быть вычислено 5 раз. Если речь пойдет о нахождении не F(8), а F(100), то МЦНМО, 2007/08 учебный год Методика и содержание подготовки учащихся к олимпиадам по программированию. Дистанционный курс. количество дублирующих друг друга вычислений будет еще больше. Очевидно, что такое решение не является оптимальным с точки зрения времени работы программы, несмотря на простоту рекурсивной реализации. С другой стороны, рекурсивная идея решения (то есть возможность выразить любое значение из этого ряда через предыдущие) заложена в самом определении ряда. Чтобы выйти из такого «затруднения» пожертвуем памятью — будем хранить столько предыдущих (уже вычисленных значений), сколько требуется для решения задачи и при этом заменим рекурсивную функцию циклом. a:=1; b:=1; for i:=3 to k do begin F:=a + b; a:=b; b:=F; end; После исполнения такого цикла (естественно, предполагается что используемая система программирования исполняет цикл от меньшего значения к большему ноль раз) F будет равно искомому числу. При этом для вычисления каждого нового значения достаточно хранить только два предыдущих. По сути, в этом и состоит идея метода, который называется «динамическое программирование». Также как и в при написании рекурсивного решения, нужны две четко определенные вещи: правило по которому на основе предыдущих решений строится новое, и условие, при котором решение очевидно. Отличие состоит в том, что решения задачи в более простых случаях хранятся в памяти (в массивах, двумерных массивах или иным способом). Пример. Дана полоска длины N клеток. В крайней левой клетке в начальный момент времени находится фишка (см. рисунок). За один ход фишка может перемещаться вправо на любое количество клеток от 1 до k. Определите количество способов переместить фишку в крайнюю правую клетку полоски. ☻ На рисунке показан один из таких способов перемещения фишки при k=5. Данная задача может быть переформулирована так: найти количество способов представить число N в виде суммы, слагаемые в которой не превышают k, при этом суммы с разным порядком слагаемых будут считаться разными. При N=1 решение очевидно: существует ровно один способ оставить фишку на том же месте. При N=2 решение также очевидно: переместить фишку в соседнюю клетку можно также МЦНМО, 2007/08 учебный год Методика и содержание подготовки учащихся к олимпиадам по программированию. Дистанционный курс. ровно одним способом1. Пусть существуют правильные решения для всех полосок длины до L включительно и они хранятся в массиве S[1..L] (на самом деле не всегда нужно хранить все предыдущие решения, но пока «для страховки» предположим, что они хранятся). Требуется определить по этим данным количество способов попасть в клетку с номером L+1 (на рисунке приведено состояние массива при k=5 и L=7, фишка находится в той клетке, для которой необходимо построить решение). 2 4 8 16 31 ☻ По условию задачи фишка перемещается на любое количество клеток от 1 до k. Это значит, что в рассматриваемую клетку фишка может попасть только из тех, расстояние от которых до нее не превышает k (на рисунке выделены цветом для k=5). 1 1 1 1 2 4 8 16 31 ☻ Рассмотрим путь из соседней слева клетки в текущую: 1 1 2 4 8 16 31 ☻ Очевидно, что существует ровно один способ попасть из клетки с номером L в клетку с номером L+1. При этом у нас есть 31 (для нашего примера) путь, который оканчивается в клетке с номером L. Это значит, что и для клетки с номером L+1 будет 31 путь, проходящий через клетку с номером L. Но в клетку с номером L+1 можно также попасть и из клетки с номером L-1: 1 1 2 4 8 16 31 ☻ Рассуждая по аналогии, к найденным прежде путям надо добавить 16, проходящих через клетку с номером L-1. Продолжим наши рассуждения для клетки с номером L-2: 1 1 2 4 8 16 31 ☻ Существует ровно один способ напрямую попасть из клетки с номером L-2 в клетку с номером L+1 (обратите внимание, что нас интересует только прямой путь; пути, проходящие через другие клетки уже посчитаны на предыдущих шагах). При этом в клетку с номером L-2 можно попасть 8 способами. Это значит, что всего есть 8 путей, ведущих напрямую из клетки с номером L-2 в клетку с номером L+1. Эти 8 путей необходимо прибавить к уже найденным. Продолжая рассуждения по аналогии мы получим правило: количество путей, ведущих в клетку с номером L+1 равно сумме решений для предыдущих k клеток (разумеется, если они существуют). То есть, заполнив массив S[] по формуле: S[L+1]:=S[L] + S[L-1]+...+S[L-k], в элементе номер N мы получим ответ к задаче. Программа для решения этой задаче может выглядеть, например, так: const N = 20; {длина полоски} 1 Здесь можно обратить внимание на аналогию с базой индукции и с терминальным условием рекурсии. МЦНМО, 2007/08 учебный год Методика и содержание подготовки учащихся к олимпиадам по программированию. Дистанционный курс. var S : array [1..N] of longint; k, L, i : integer; begin read (k); S[1]:=1; { ответы для первых двух клеток очевидны} S[2]:=1; for L:=3 to N do begin S[L]:=0; i:=L-1; while (i>0)and(i>=L-k) do begin S[L]:=S[L] + S[i]; {для всех последующих клеток вычисляем} dec(i); {ответ как сумму k предыдущих } end; end; Write(S[N]); end. Пример. Задача о «хромом короле». Дана доска, A x B клеток. В правом верхнем углу ее находится «хромой» шахматный король — в отличие от обычного короля он может ходить только на одну клетку вниз или на одну клетку вправо. Определить количество разных путей, по которым король может попасть в правую нижнюю клетку. На рисунке изображено несколько вариантов такого пути «хромого короля». Разные продолжения пути показаны разными цветами. K Задача является практически двухмерным случаем предыдущей. Для начала заметим, что в МЦНМО, 2007/08 учебный год Методика и содержание подготовки учащихся к олимпиадам по программированию. Дистанционный курс. любую из клеток верхней строки и левого столбца король может попасть только одним способом (база индукции, терминальное условие рекурсии). K 1 1 1 1 1 1 1 1 1 1 Теперь построим динамический переход (сформулируем шаг индукции, правило сведения задачи к более простой). Допустим, что у нас есть готовые правильные решения для некоторого прямоугольного участка доски, который начинается в ее левом верхнем углу (на рисунке показан такой участок размерами 4 x 3). Вернее для всего участка, за исключением его правой нижней клетки: K 1 1 1 1 1 2 3 1 3 6 1 4 ? 1 1 1 В клетку, отмеченную знаком вопроса можно попасть либо из соседней сверху, либо из соседней слева. Причем, и из той, и из другой — ровно одним способом. Для данного рисунка это означает, что существует 6 путей через верхнюю клетку и 4 — через левую. Всего 10 (по аналогии с предыдущей задачей). Пусть промежуточные решения хранятся в массиве S[1..n, 1..m]. Тогда S[i,j] определяется по формуле S[i,j]:=S[i-1, j] + S[i, j-1] (при i<>1, j<>1). Далеко не всегда параметры, от которых зависит решение, являются очевидными. Не всегда первая попавшаяся самая очевидная формула перехода будет самой оптимальной. Иногда их нужно поискать, попробовать решение задачи с разных сторон. Именно этим метод динамического программирования отличается от многих других стандартных методов и алгоритмов. МЦНМО, 2007/08 учебный год Методика и содержание подготовки учащихся к олимпиадам по программированию. Дистанционный курс. Пример2. Определите количество способов представить число N в виде суммы положительных слагаемых, каждое из которых строго меньше предыдущего. Отдельное число можно тоже считается «суммой» из одного единственного слагаемого. Общее количество слагаемых в сумме не ограничено. Например, число 6 можно представить такими суммами: 6; 5+1; 4+2; 3+2+1. Применим метод динамического программирования. Для небольших чисел, таких как 1 или 2 — решение очевидно. Для N=1 ответ равен единице, для N=2 ответ также равен единице3. Построим динамический переход. Пусть существуют правильные решения для всех чисел от 1 до N-1 и требуется на их основе сделать решение для числа N (на рисунке под решением для конкретного N приведен комментарий как оно получено). N=1 N=2 N=3 N=4 N=5 N=6 1 1 2 2 3 4 1 2 3 2+1 4 3+1 5 4+1 3+2 ? 6 5+1 4+2 3+2+1 Рассмотрим пример. Число 7 (для которого строится решение) можно представить, к примеру, как 7=6+1. Однако, попытка решить задачу аналогично первому примеру (о фишке) будет неудачной. На первый взгляд казалось бы: есть 4 способа получить число 6, и ровно один способ прибавить к 6 единицу. Однако, по условию задачи все слагаемые должны быть различны. Это значит, что необходимо взять не все способы разложить число 6, а только те, где еще нет числа 1. А таких способов не 4, а 2, но это никак не отражено в нашей одномерной таблице. Аналогично, если представить 7 как 5+2, то нужно учитывать не все способы разложения числа 5, а только те, где не было числа 2. Таким образом мы приходим к выводу, что помимо количества способов представить то или иное число в виде суммы в данной задаче необходимо хранить сведения о том, каким образом (из каких слагаемых) эта сумма получена. Проведем некоторые рассуждения в общем виде. Пусть N можно представить как сумму N= a + b. Пусть, в свою очередь, число b можно представить несколькими способами. И требуется выбрать те из этих способов, в которых гарантировано нет числа a. Для этого введем очень простое требование: будем учитывать только те способы разложения числа b, в которых участвуют только слагаемые строго меньше чем а. В этом случае количество таких сумм будет равно количеству разложений числа b со слагаемыми не более a. И для решения данной задачи будет необходимо хранить два параметра — само число и максимальное слагаемое, для которого построено промежуточное решение. 2 Для данной темы, впрочем как и для рекурсии, может понадобиться разбор большого количества примеров. 3 В задаче не конкретизируется, считается ли сумма из одного слагаемого «нормальной». Для определенности в данном примере будем считать, что сумма из одного слагаемого имеет право на существование. Если отбросить такие суммы, то суть решения практически не изменится. МЦНМО, 2007/08 учебный год Методика и содержание подготовки учащихся к олимпиадам по программированию. Дистанционный курс. Пусть массив S [1..n, 1..n] хранит решения. Тогда S[a,b] — количество способов разложить число a в сумму со слагаемыми не более b. В этом случае формула динамического перехода будет выглядеть так: S[a, b]:=S[a, b-1] + S[a-b, b-1] Первая часть формулы S[a, b-1] просто добавляет к текущему решению все способы разложить число a с меньшими слагаемыми (не более b-1). Вторая часть S[a-b, b-1] взята из предыдущих рассуждений и представляет собой количество способов записать число a-b с максимальным слагаемым до b-1. Принцип хранения данных в этой задаче показан на рисунке. Максимальное слагаемое 1 2 3 4 5 6 7 8 9 10 1 1 2 0 1 3 0 1 2 N 4 0 0 1 2 5 0 0 1 2 3 Количество способов представить данное число N (на примере 6) в виде сумм, в которых максимальное слагаемое не превышает данное (на примере 4). 6 0 0 1 2 3 4 7 0 0 0 2 3 4 5 8 0 0 0 1 3 4 5 6 9 0 0 0 1 3 5 6 7 8 10 0 0 0 1 3 5 7 8 9 10 В первом столбце запишем все числа от 1 до того, до данного нам N. В первой строке — различные слагаемые, с которых могут начинаться суммы. На пересечении строки и столбца указывается число способов представить данное число N (номер строки) в виде суммы слагаемых, из которых наибольшее не превосходит данное (номер столбца). В примере, в 6-й строке 4-м столбце находится число 2. Это значит, что если ограничить максимальное слагаемое числом 4, то у нас будет всего 2 способа записать сумму: 1+2+3 и 2+4. Остальные способы требуют больших слагаемых, поэтому будут учтены позже. Заполнив весь двухмерный массив по формуле S[a, b]:=S[a, b-1] + S[a-b, b-1] в ячейке с координатами (n,n) мы получим искомое количество способов. Еще более сложными задачами на метод динамического программирования являются МЦНМО, 2007/08 учебный год Методика и содержание подготовки учащихся к олимпиадам по программированию. Дистанционный курс. такие, где в результате решения получается не готовый ответ, а какая-то величина (или массив) по которой необходимо собрать ответ с помощью дополнительных вычислений (например, перебрав в определенном порядке элементы массива). Такие задачи относятся уже к достаточно высокому олимпиадному уровню, и подготовкой такого уровня уже занимаются не столько в рамках школы, сколько в рамках сборов разного уровня. Посему задачи такой сложности в этом курсе рассмотрены не будут. Задачи 1. Дан массив из N элементов, каждый из которых является либо нулем, либо единицей. Определить количество групп единиц, разделенных нулями. 2. Определите, сколько существует способов расположить в массиве из N элементов нули и единицы так, чтобы не было двух подряд идущих единиц. МЦНМО, 2007/08 учебный год