Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

KN_Vse_lektsii / algan27

.pdf
Скачиваний:
83
Добавлен:
23.02.2015
Размер:
348.6 Кб
Скачать

Алгоритмический анализ. Лекция 27.

Программирование на Scheme

Динамическое программирование

Динамическое программирование в математике и теории вычислительных систем — метод решения задач с оптимальной подструктурой и перекрывающимися подзадачами, который намного эффективнее, чем решение «в лоб» (brute force).

Словосочетание динамическое программирование впервые было использовано в 1940-х годах Ричардом Беллманом для описания процесса нахождения решения задачи, где ответ на одну задачу может быть получен только после решения задачи, «предшествующей» ей. Вклад Беллмана в динамическое программирование был увековечен в названии уравнения Беллмана, центрального результата теории динамического программирования, который переформулирует оптимизационную задачу в рекурсивной форме.

Слово «программирование» в словосочетании «динамическое программирование» в действительности к традиционному программированию (написанию кода) почти никакого отношения не имеет и происходит от словосочетания «математическое программирование», которое является синонимом слова «оптимизация».

Довольно часто на олимпиадах встречаются задачи, провоцирующие к применению алгоритмы перебора. Но простой подсчет числа вариантов убеждает в неэффективности такого подхода. Для решения таких задач используется метод динамического программирования. Суть его заключается в том, что для поиска решения поставленной задачи решается похожая (или похожие), но более простая. При этом осуществляется переход к еще более простым и так далее, пока не доходят до тривиальной.

Динамическое программирование обычно применяется к задачам, в

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

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

В типичном случае динамическое программирование применяется к задачам оптимизации. У такой задачи может быть много возможных

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

Из предыдущего рассуждения видно, что решение можно оформить рекурсивно. Но простое применение этого приема очень легко может привести к переполнению стека. Необходимо позаботиться об оптимизации рекурсивных проходов и не вычислять одно и то же значение несколько раз, сделать так называемое отсечение. Можно вообще отказаться от рекурсии и решать задачу "наоборот" — прежде "решить" тривиальные случаи, а затем переходить ко все более сложным. В авторских решениях подобных задач почти всегда встречается второй путь (он несколько быстрее), но в этом занятии рассмотрим оба — первый гораздо доступнее для понимания.

Типовой алгоритм решения задач методом динамического программирования:

1.Описать строение оптимальных решений.

2.Выписать рекуррентное соотношение, связывающие оптимальные значения параметра для подзадач.

3.Двигаясь снизу вверх, вычислить оптимальное значение параметра для подзадач.

4.Пользуясь полученной информацией, построить оптимальное решение.

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

Рассмотрим применение метода динамического программирования на примере некоторых задач.

Задача о дорожке

Пусть у нас есть дорожка шириной 2 и длиной N. Нужно выложить ее тротуарной плиткой размера 1*2. Требуется определить количество различных способов.

Например, при N=3 имеем три варианта:

1

2

3

 

 

 

 

 

 

 

 

 

1

2

2

 

 

 

 

 

 

1

1

3

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

1

2

3

 

 

 

 

 

 

 

 

 

1

3

3

 

 

 

 

 

 

2

2

3

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Решать эту задачу полным перебором – бесперспективно!

Применим метод динамического программирования.

Левая верхняя клетка, как и все остальные в итоге должна быть закрыта одной из плиток. При этом у нас есть всего два варианта: плитка располагается вертикально или горизонтально.

При вертикальном расположении плитки свободным остается поле 2*(N-1). При горизонтальном положении у нас не остается выбора для размещения плитки, которая закрыла бы левый нижний угол, значит, для дальнейшего решения остается поле 2*(N-2).

Таким образом, если обозначить F(N) – число вариантов размещения плиток на дорожке длины N (а это потом и будет ответом нашей задачи), то имеем формулу:

F(N) = F(N-1) + F(N-2)

При этом легко можно посчитать, что F(1) = 1 и F(2) = 2.

Внимательно присмотревшись к этой закономерности, мы узнаем в ней известную последовательность чисел Фибоначчи:

1, 2, 3, 5, 8, 13, 21, 34 и т.д.

Каждое число равно сумме двух предыдущих и все начинается с 1 и 2, как и в нашем случае.

Конечно, можно строго следуя определению, написать рекурсивный алгоритм:

(define (fibb n)

(if (<= n 1) 1 (+ (fibb (- n 1)) (fibb (- n 2)))))

При этом на шестом-седьмом десятке программа 'подвесит' самый быстрый компьютер. Попробуем разобраться, почему так происходит?

Для вычисления F(40) мы сперва вычисляем F(39) и F(38). Причем F(38) мы считаем ―по новой‖, ―забывая‖, что уже вычислили его, когда считали F(39).

То есть наша основная ошибка в том, что значение функции при одном и том же значении аргумента считается много (слишком много!) раз.

Лучше использовать итерационный алгоритм:

(define (fib n)

(define (iter k a b) (if (= k n) b

(iter (+ k 1) b (+ a b)))) (iter 1 1 1))

Мячик на лесенке

На вершине лесенки, содержащей N ступенек, находится мячик, который начинает прыгать по ним вниз, к основанию. Мячик может прыгнуть на следующую ступеньку, на ступеньку через одну или через 2. (То есть, если мячик лежит на 8-ой ступеньке, то он может переместиться на 5-ую, 6-ую или 7-ую.) Определить число всевозможных "маршрутов" мячика с вершины на землю.

Пусть мячик находится на некоторой ступеньке с номером X. Тогда он может спрыгнуть на ступеньки с номерами X - 1, X - 2 и X - 3. Если мы введем функцию F(X), которая определяет число маршрутов со ступеньки X до земли, то

F(X) = F(X – 1) + F(X – 2) + F(X – 3).

Остается просчитать вручную граничные значения: F(1) = 1, F(2) = 2 (очевидно), F(3) = 4 (3–2–1–0, 3–2–0, 3-1–0, 3–0). Все остальное уже сделано в предыдущей задаче. Чтобы не загромождать функцию, можно присваивания граничных значений сделать заранее, тогда отсечение произойдет автоматически:

(define (ball n)

(define (iter k a b c)

(if (= k n) b

(iter (+ k 1) b c (+ a b c))))

(cond

((= n 1) 1)

((= n 2) 2)

(else (iter 3 1 2 4))))

Черепашка

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

Для каждой клетки таблицы определим функцию стоимости оптимально пути F(i,j), где i – строки, а j – номер столбца нашей таблицы. Таким образом, эта функция для левого верхнего угла F(1, 1) и будет окончательным ответом нашей задачи. Заметим, что в любой клетке значение определяется по соседней справа (если бы мы пошли туда) и по соседней снизу (если бы пошли вниз). Будем все значения функции F хранить в виде дополнительной таблицы. Заполнять ее будем справа налево и снизу вверх, например:

Исходная таблица

Значения функции F

2

8

7

2

 

 

 

 

3

1

4

5

 

 

 

 

5

4

2

1

 

 

 

 

1

7

3

6

 

 

 

 

19

22

20

14

 

 

 

 

17

14

13

12

 

 

 

 

18

13

9

7

 

 

 

 

17

16

9

6

 

 

 

 

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

Алгоритм полного перебора всех маршрутов требовал бы порядка 2N действий, а нам удалось обеспечить всего N2, где N – размер таблицы.

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

; Функция делает список из сумм элементов, ;расположенных до заданного включительно.

(define (lst-sum lst)

(reverse (foldl (lambda (x rez) (cons (+ x (car rez)) rez)) (list (car lst)) (cdr lst))))

(define (matr-sum A)

(reverse (foldl (lambda (lst rez) (cons

(reverse

(foldl (lambda (x y str) (if (< y (car str))

(cons (+ x y) str)

(cons (+ x (car str)) str))) (list (+ (car lst) (caar rez))) (cdr lst) (cdar rez)))

rez))

(list (lst-sum (car A))) (cdr A))))

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

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

Задача о счастливых билетах

Пусть билет содержит 6 цифр. Обычно называют его счастливым, если сумма первых трех цифр равна сумме последних трех. Будем считать, что наши билеты имеют 2*N цифр. Требуется посчитать количество счастливых билетов для заданного N.

Здесь тоже попытаемся обойтись без перебора. Но в этом случае просто так не удается свести задачу для данного N к задачам для меньших N. Необходимо ввести новую вспомогательную характеристику.

Обозначим количество n-значных номеров с суммой цифр, равной k, через Fn(k).

Найдем F1(k). Нужно определить, сколько однозначных чисел имеет сумму цифр k. Но для однозначного числа сумма цифр совпадает с самим числом. Значит

F1(k) = {

1, если k принимает от 0 ..

0 при других значениях k.

Предположим теперь, что мы уже знаем значения Fn–1(k) для всех k. Попробуем выразить через них Fn(k). Другими словами, попробуем найти количество n-значных номеров с суммой цифр, равной k, предполагая, что для (n–1)-значных номеров задача уже решена.

Пусть первой цифрой n-значного номера является число t. Чтобы сумма цифр этого номера была равна k, остальные его цифры должны в сумме дать k – t . Таких (n–1)-значных номеров существует Fn–1(k – t ). Цифра t может быть любым целым однозначным числом, не превосходящим k (то есть 0 ≤ t ≤ 9 , t≤ k ), и каждой из этих цифр соответствует Fn–1(k – t ) n-значных номеров с суммой цифр, равной k, причѐм все эти номера различны. Значит, всего таких номеров будет

Fn(k) = ∑ i=0..9 Fn-1(k-i) (*)

причѐм, если k < 9 , то для t> k соответствующие значения Fn– 1(k – t ) мы будем считать равными нулю.

По формуле (*) можно вычислить значения Fn(k) для всех k, если известны значения Fn–1(k). А так как значения F1(k) мы уже знаем, то задача решена!

Заметим, что при вычислении очередного значения функции можно не вычислять сумму десяти элементов по формуле (*), а производить вычисления по схеме:

Fn(k) = Fn(k-1) + Fn-1(k) – Fn-1(k-10)

Нагляднее всего процесс решения можно показать на таблице.

Таблица значений Fn(k)

K

n =

n =

n =

n =

0

1

1

1

1

1

1

2

3

4

2

1

3

6

10

3

1

4

10

20

4

1

5

15

35

5

1

6

21

56

6

1

7

28

84

7

1

8

36

120

8

1

9

45

165

9

1

10

55

220

10

 

9

63

282

11

 

8

69

348

12

 

7

73

415

13

 

6

75

480

14

 

5

75

540

15

 

4

73

592

16

 

3

69

633

17

 

2

63

660

18

 

1

55

670

19

 

 

45

660

20

 

 

36

633

21

 

 

28

592

22

 

 

21

540

23

 

 

15

480

24

 

 

10

415

25

 

 

6

348

26

 

 

3

282

27

 

 

1

220

28

 

 

 

165

29

 

 

 

120

30

 

 

 

84

31

 

 

 

56

32

 

 

 

35

33

 

 

 

20

34

 

 

 

10

35

 

 

 

4

36

 

 

 

1

Теперь по таблице можно подсчитать число счастливых билетов с (2*N)-значными номерами.

Для этого все числа в N-м столбце таблицы нужно возвести в квадрат и сложить.

Например, мы получим для двузначных номеров, т.е. для

N = 1, ответ = 10,

для четырехзначных номеров, N = 2, ответ = 670,

для шестизначных номеров, N = 3, ответ = 55 252,

для восьмизначных номеров, N = 4, ответ = 4 816 030.

Соседние файлы в папке KN_Vse_lektsii