Скачать презентацию Рекурсия и методы избавления от неё Виды рекурсии Скачать презентацию Рекурсия и методы избавления от неё Виды рекурсии

FP Recursion (final).pptx

  • Количество слайдов: 56

Рекурсия и методы избавления от неё Виды рекурсии, мемоизация, динамическое программирование, стек отложенных заданий, Рекурсия и методы избавления от неё Виды рекурсии, мемоизация, динамическое программирование, стек отложенных заданий, continuation-passing style, ленивые вычисления

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

Рекурсия позволяет получать формулы исходя из определения. Например, длина списка может быть выражена так: Рекурсия позволяет получать формулы исходя из определения. Например, длина списка может быть выражена так: let rec length list = match list with | [] -> 0 //(*) | _: : tail -> 1 + length tail //(**) Фактически, здесь написано, что длина пустого списка равна 0 (*), а длина непустого списка равна 1 + длина списка без первого элемента (**).

При записи рекурсии всегда выделяют: • Базу рекурсии –параметры функций, при которой возвращается некое При записи рекурсии всегда выделяют: • Базу рекурсии –параметры функций, при которой возвращается некое значение без рекурсивного вызова. • Шаг рекурсии – это все случаи при которых происходит рекурсивный вызов. let rec length list = match list with | [] -> 0 //база | _: : tail -> 1 + length tail //шаг

Хвостовая рекурсия Рекурсия называется хвостовой, если рекурсивный вызов идёт в конце кода рекурсивной функции Хвостовая рекурсия Рекурсия называется хвостовой, если рекурсивный вызов идёт в конце кода рекурсивной функции и не включён в какоелибо выражение. Хвостовая рекурсия автоматически преобразуется компиляторами C#, F#, Haskell, Scala, Scheme, … в итерацию. Нехвостовая рекурсия может привести к переполнению стека.

> length [1. . 3]; ; val it : int = 3 > length > length [1. . 3]; ; val it : int = 3 > length [1. . 1000000]; ; Process is terminated due to Stack. Overflow. Exception.

Аккумуляторы и свёртки Чтобы преобразовать нашу функцию в хвосторекурсивную, воспользуемся приёмом, который называется паттерном Аккумуляторы и свёртки Чтобы преобразовать нашу функцию в хвосторекурсивную, воспользуемся приёмом, который называется паттерном аккумуляции. let length' list = let rec helper list acc = match list with | [] -> acc | _: : tail -> helper tail (acc + 1) helper list 0 > length' [1. . 1000000]; ; val it : int = 1000000

Такой подход используется в функциональном программировании настолько часто, что был создан специальный комбинатор, с Такой подход используется в функциональном программировании настолько часто, что был создан специальный комбинатор, с помощью которого такие функции можно создавать автоматически. Он называется свёрткой (fold) и является реализацией концепции из теории категорий – катаморфизма.

Свёртка let length. Folded list = List. fold (fun acc _ -> acc + Свёртка let length. Folded list = List. fold (fun acc _ -> acc + 1) 0 list Мы получили хвосторекурсивную функцию, вычисляющую длину списка: > length. Folded [1. . 1000000]; ; val it : int = 1000000

fold – это ФВП следующего типа: fold : (‘state -> ‘a -> ‘state) -> fold – это ФВП следующего типа: fold : (‘state -> ‘a -> ‘state) -> ‘a list -> ‘state А сам fold можно записать так: let rec fold f init list = match list with | [] -> init | x: : xs -> fold f (f init x) xs val fold : ('a -> 'b -> 'a) -> 'a -> 'b list -> 'a

Возвращение замыкания Сравните: let is. Nebraska. City_bad city = let cities = printfn Возвращение замыкания Сравните: let is. Nebraska. City_bad city = let cities = printfn "Creating cities Set" ["Bellevue"; "Omaha"; "Lincoln"; "Papillion"] |> Set. of. List cities. Contains(city) let is. Nebraska. City_good = let cities = printfn "Creating cities Set" ["Bellevue"; "Omaha"; "Lincoln"; "Papillion"] |> Set. of. List fun city -> cities. Contains(city)

Второй пример – это не определение функции, а определение значения, равного замыканию. Таким образом, Второй пример – это не определение функции, а определение значения, равного замыканию. Таким образом, во втором примере множество будет построено только один раз и передано в замыкание, которое мы назовём is. Nebraska. City_good. В первом же примере мы определили функцию, которая будет строить множество каждый раз.

> let is. Nebraska. City_bad city = . . . val is. Nebraska. City_bad > let is. Nebraska. City_bad city = . . . val is. Nebraska. City_bad : string -> bool > is. Nebraska. City_bad "Lincoln"; ; Creating cities Set val it : bool = true > is. Nebraska. City_bad "Washington"; ; Creating cities Set val it : bool = false > let is. Nebraska. City_good = . . . Creating cities Set val is. Nebraska. City_good : (string -> bool) > is. Nebraska. City_good "Lincoln"; ; val it : bool = true > is. Nebraska. City_good "Washington"; ; val it : bool = false is. Nebraska. City_good быстрее, чем is. Nebraska. City_bad. По материалам: http: //en. wikibooks. org/wiki/F_Sharp_Programming/Caching

Мемоизация В ФП большая часть функций – чистые, т. е. их вызов с одними Мемоизация В ФП большая часть функций – чистые, т. е. их вызов с одними и теми же параметрами всегда вернёт один и тот же ответ. Чтобы ускорить вычисления, можно запомнить этот ответ в ассоциативном массиве – тогда при повторном вызове функции с теми же самыми параметрами мы можем просто вернуть уже готовый ответ.

Пример open System. Collections. Generic let slow. Function t = System. Threading. Thread. Sleep(t Пример open System. Collections. Generic let slow. Function t = System. Threading. Thread. Sleep(t * 1000) t let memoize (f : 'a -> 'b) = let cache = new Dictionary<'a, 'b>() let memoized. F x = match cache. Try. Get. Value(x) with | true, y -> y | false, _ -> let y = f x cache. Add(x, y) y memoized. F

let memoized. Slow. Function = memoize slow. Function val slow. Function : int -> let memoized. Slow. Function = memoize slow. Function val slow. Function : int -> int val memoize : ('a -> 'b) -> ('a -> 'b) when 'a : equality val memoized. Slow. Function : (int -> int) > #time; ; --> Timing now on > slow. Function 3; ; Real: 00: 03. 000, … val it : int = 3 > slow. Function 3; ; Real: 00: 02. 999, … val it : int = 3 > memoized. Slow. Function 3; ; Real: 00: 03. 002, … val it : int = 3 > memoized. Slow. Function 3; ; Real: 00: 00. 000, … val it : int = 3

Упражнение Напишите ФВП для мемоизации на C#. Используйте лямбда-выражения и делегат Func<T 1, T Упражнение Напишите ФВП для мемоизации на C#. Используйте лямбда-выражения и делегат Func: static Func Memoize(Func f) { . . . return x => {. . . ; }; } var memoized = Memoize(Slow. Function);

Мемоизация рекурсивных функций Если мы хотим мемоизовать рекурсивную функцию, например, вычисление чисел Фибоначчи: let Мемоизация рекурсивных функций Если мы хотим мемоизовать рекурсивную функцию, например, вычисление чисел Фибоначчи: let rec fib n = if n < 2 then 1 else fib (n-1) + fib (n-2) Мы сталкиваемся с проблемой: в мемоизованной версии функции рекурсивные вызовы будут ссылаться на немемоизованную версию, и наши усилия будут тщетны.

let wrong. Memoized. Fib = memoize fib > wrong. Memoized. Fib 35; ; Real: let wrong. Memoized. Fib = memoize fib > wrong. Memoized. Fib 35; ; Real: 00: 00. 220 val it : int = 14930352 > wrong. Memoized. Fib 35; ; Real: 00: 00. 000 val it : int = 14930352 > wrong. Memoized. Fib 34; ; Real: 00: 00. 141 val it : int = 9227465

Мы можем переписать мемоизуемую функцию в ФВП, принимающую в качестве аргумента функцию, которую необходимо Мы можем переписать мемоизуемую функцию в ФВП, принимающую в качестве аргумента функцию, которую необходимо вызвать в рекурсии. Мы просто убираем слово rec и повторяем имя функции: let fib' n = if n < 2 then 1 else fib' (n-1) + fib' (n-2) val fib' : (int -> int) -> int Рекурсия исчезла – теперь в ветке else мы вызываем функцию, переданную в качестве первого параметра. Такую функцию нельзя использовать в чистом виде, но она мемоизуется:

let memoize' f = let cache = new Dictionary<'a, 'b>() let rec memoized. F let memoize' f = let cache = new Dictionary<'a, 'b>() let rec memoized. F x = match cache. Try. Get. Value(x) with | true, y -> y | false, _ -> let y = f memoized. F x cache. Add(x, y) y memoized. F let memoized. Fib = memoize' fib‘

> fib 35; ; Real: 00: 00. 204 val it : int = 14930352 > fib 35; ; Real: 00: 00. 204 val it : int = 14930352 > fib 34; ; Real: 00: 00. 132 val it : int = 9227465 > memoized. Fib 35; ; Real: 00: 00. 004 val it : int = 14930352 > memoized. Fib 35; ; Real: 00: 00. 000 val it : int = 14930352 > memoized. Fib 34; ; Real: 00: 00. 002 val it : int = 9227465

Мы можем объявить первоначальную рекурсивную функцию как лямбдавыражение и послать его как аргумент в Мы можем объявить первоначальную рекурсивную функцию как лямбдавыражение и послать его как аргумент в первую версию комбинатора мемоизации: let rec fib. Fast = memoize ( fun n -> if n < 2 then 1 else fib. Fast (n-1) + fib. Fast (n-2) ) Таким образом рекурсивные вызовы будут заменены на мемоизованную версию

Замечания - Мемоизация позволяет существенно ускорить многие функции - Тем не менее, за скорость Замечания - Мемоизация позволяет существенно ускорить многие функции - Тем не менее, за скорость мы платим памятью. В более сложных случаях необходимо учитывать частоты вызовов функции с определёнными аргументами и удалять редко вызываемые записи из ассоциативного массива или ограничивать размер ассоциативного массива.

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

Формализация Формализация

 •

open System. Numerics let rec Ball. On. Stair = memoize ( function | 1 open System. Numerics let rec Ball. On. Stair = memoize ( function | 1 -> 1 I | 2 -> 2 I | 3 -> 4 I | n -> Ball. On. Stair (n - 1) + Ball. On. Stair (n - 2) + Ball. On. Stair (n - 3) ) val Ball. On. Stair : (int -> Big. Integer) Ball. On. Stair 1000; ; Real: 00: 00. 008 val it : Big. Integer = 27588428077664862526158924116561586451331001496526962103516018450 3639297. . .

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

Условия применимости ДП 1. Оптимальная подструктура – оптимальные решения подзадач могут быть использованы для Условия применимости ДП 1. Оптимальная подструктура – оптимальные решения подзадач могут быть использованы для оптимального решения задачи 2. Перекрывающиеся подзадачи – рекурсивный алгоритм должен решать одни и те же подзадачи несколько раз

Оптимальная подструктура Обычно выражается рекуррентным соотношением, как в задаче про мячик. Например, если в Оптимальная подструктура Обычно выражается рекуррентным соотношением, как в задаче про мячик. Например, если в графе существует путь с минимальным весом P, в который входят вершины a, b и c, то отрезки пути P между a и b и с также являются оптимальными путями между соответствующими вершинами – на этом основан алгоритм Беллмана. Форда.

Перекрывающиеся подзадачи • Перекрывающиеся подзадачи •

let Ball. On. Stair. DP n = let cache = if n > 3 let Ball. On. Stair. DP n = let cache = if n > 3 then Array. zero. Create n else Array. zero. Create 3 cache. [0] <- 1 I cache. [1] <- 2 I cache. [2] <- 4 I let mutable idx = 3 while idx < n do cache. [idx] <- cache. [idx - 1] + cache. [idx - 2] + cache. [idx - 3] idx <- idx + 1 cache. [n - 1]

Упражнение Черепашка На квадратной доске расставлены целые неотрицательные числа. Черепашка, находящаяся в левом верхнем Упражнение Черепашка На квадратной доске расставлены целые неотрицательные числа. Черепашка, находящаяся в левом верхнем углу, мечтает попасть в правый нижний. При этом она может переползать только в клетку справа или снизу и хочет, чтобы сумма всех чисел, оказавшихся у нее на пути, была бы максимальной. Определить эту сумму. Формат входных данных Первая строка — N — размер доски. Далее следует N строк, каждая из которых содержит N целых чисел, представляющие доску. Формат выходных данных Одно число — максимальная сумма.

 S 20 15 17 5 30 7 10 20 50 2 F S S 20 15 17 5 30 7 10 20 50 2 F S 20 35 52 5 25 F

private static void Turtle() { var grid = Input. Data. For. Turtle(); for (int private static void Turtle() { var grid = Input. Data. For. Turtle(); for (int x = 1; x < grid. Get. Length(0); x++) for (int y = 1; y < grid. Get. Length(1); y++) if (grid[x - 1, y] > grid[x, y - 1]) grid[x, y] = grid[x - 1, y] + grid[x, y]; else grid[x, y] = grid[x, y - 1] + grid[x, y]; Console. Write. Line("Maximum weight: {0}", grid[grid. Get. Length(0) - 1, grid. Get. Length(1) - 1]); } Для упрощения кода мы используем дополнительную строку сверху и столбец слева, заполненные нулями.

Решение с помощью мемоизации let grid = array 2 D [| [|0; 0; 0|]; Решение с помощью мемоизации let grid = array 2 D [| [|0; 0; 0|]; [|0; 0; 20; 15; 17|]; [|0; 5; 30; 7; 10|]; [|0; 20; 50; 2; 0|] |] let rec maximum. Path = memoize ( function | 0, 0 -> 0 | 0, y -> maximum. Path(0, y - 1) + grid. [0, y] | x, 0 -> maximum. Path(x - 1, 0) + grid. [x, 0] | x, y -> if (maximum. Path(x - 1, y) > maximum. Path(x, y - 1)) then maximum. Path(x - 1, y) + grid. [x, y] else maximum. Path(x, y - 1) + grid. [x, y] ) let answer = maximum. Path(grid. Get. Length(0) - 1, grid. Get. Length(1) - 1)

Стек отложенных заданий Рассмотрим бинарные деревья поиска: Св-во бинарных деревьев поиска: где v – Стек отложенных заданий Рассмотрим бинарные деревья поиска: Св-во бинарных деревьев поиска: где v – некая вершина дерева, v(l) – левое поддерево v v(r) – правое поддерево v, а операция сравнения между множеством элементов и элементом x = истине, если справедлива при сравнении каждого элемента множества с x.

Применения BST Сложность Средняя Худший случай Поиск элемента O(log n) O(n) Вставка элемента O(log Применения BST Сложность Средняя Худший случай Поиск элемента O(log n) O(n) Вставка элемента O(log n) O(n) Удаление элемента O(log n) O(n) BST хорошо подходят для реализации множеств, мультимножеств и ассоциативных массивов. Сами же деревья также применяются для представления иерархических данных, например дерево директорий файловой системы или дерево игровых состояний.

Дерево как алгебраический тип данных Дерево – это абстрактный тип данных, могущий иметь множество Дерево как алгебраический тип данных Дерево – это абстрактный тип данных, могущий иметь множество различных физических реализаций. Мы реализуем дерево как обобщённый (aka полиморфный) рекурсивный алгебраический тип данных: type Tree<'a> = | Node of 'a * Tree<'a> | Empty

Задание: ООП-версия Попробуйте реализовать такое представление дерева с помощью ООП. — Node и Empty Задание: ООП-версия Попробуйте реализовать такое представление дерева с помощью ООП. — Node и Empty должны быть одного типа. Подумайте об альтернативе Empty в объектно -ориентированном языке. — Что вы будете использовать вместо pattern -matching’a, чтобы работать с такой структурой? — Подумайте, как можно представить любой алгебраический тип данных в ООП?

Чтобы дерево сохраняло основное свойство бинарных деревьев поиска, необходимо добавлять элементы с учётом этого Чтобы дерево сохраняло основное свойство бинарных деревьев поиска, необходимо добавлять элементы с учётом этого свойства: let rec add. Vertex v = function | Empty -> Node(v, Empty) | Node(v', left, right) as initial -> if (v = v') then initial elif (v < v') then Node(v', (add. Vertex v left), right) else Node(v', left, (add. Vertex v right)) Дерево – неизменяемая структура данных, поэтому тип функции следующий: add. Vertex : 'a -> Tree<'a> when 'a : comparison

Мы используем свёртку, чтобы получить дерево из линейной структуры данных, например, массива: let build. Мы используем свёртку, чтобы получить дерево из линейной структуры данных, например, массива: let build. Tree (seq : 'a seq) = Seq. fold (fun acc elem -> add. Vertex elem acc) Empty seq

Поиск в дереве let rec in. Tree tree x = match tree with | Поиск в дереве let rec in. Tree tree x = match tree with | Empty -> false | Node(v, _, _) when v = x -> true | Node(v, _, right) when v < x -> in. Tree right x | Node(v, left, _) when v > x -> in. Tree left x | _ -> failwith ". . . " tree x Функция хвосторекурсивна, соответственно, будет преобразована в итерацию компилятором.

Ассоциативный массив на BST Если мы будем трактовать дерево типа Tree<‘key, ‘value> как ассоциативный Ассоциативный массив на BST Если мы будем трактовать дерево типа Tree<‘key, ‘value> как ассоциативный массив с ключами типа ‘key и значениями типа ‘value, мы сможем адаптировать нашу функцию поиска, чтобы она возвращала значение по ключу: let rec in. Tree. Key tree x = match tree with | Empty -> None | Node((k, v), _, _) when k = x -> Some v | Node((k, v), _, right) when k < x -> in. Tree. Key right x | Node((k, v), left, _) when k > x -> in. Tree. Key left x | _ -> failwith ". . . " tree x

Обход дерева Дерево можно обойти в трёх различных порядках. Прямой порядок: 1. Посетить корень Обход дерева Дерево можно обойти в трёх различных порядках. Прямой порядок: 1. Посетить корень 2. Посетить левое поддерево 3. Посетить правое поддерево left-to-right depth-first traversal preorder traversal

Симметричный порядок 1. Посетить левое поддерево 2. Посетить корень 3. Посетить правое поддерево Особенно Симметричный порядок 1. Посетить левое поддерево 2. Посетить корень 3. Посетить правое поддерево Особенно важен для BST, т. к. в результате мы получим отсортированную последовательность элементов (в силу основного свойства BST) inorder traversal symmetric traversal

postorder traversal Обратный порядок 1. Посетить левое поддерево 2. Посетить правое поддерево 3. Посетить postorder traversal Обратный порядок 1. Посетить левое поддерево 2. Посетить правое поддерево 3. Посетить корень

Всё это легко выражается на F#: let rec inorder tree action = match tree Всё это легко выражается на F#: let rec inorder tree action = match tree with | Empty -> () | Node(v, left, right) -> inorder left action v inorder right action inorder : Tree<'a> -> ('a -> unit) -> unit Задание: Напишите функции для обхода в прямом и обратном порядке

Бинарная рекурсия Но возникает одна проблема – эти функции не хвосторекурсивны. Если мы захотим Бинарная рекурсия Но возникает одна проблема – эти функции не хвосторекурсивны. Если мы захотим написать хвосторекурсивные аналоги, мы столкнёмся с препятствием: эти функции используют бинарную рекурсию, поэтому паттерн аккумуляции (свёртка) неприменим в данном случае. Но мы можем написать нерекурсивный аналог этих функций. Для этого воспользуемся стеком отложенных заданий.

Мы можем запоминать то, что нам надо делать, в стеке. Вначале мы кладём на Мы можем запоминать то, что нам надо делать, в стеке. Вначале мы кладём на стек начальное условие. Затем мы запускаем цикл, который пытается очистить стек, доставая значение с вершины стека и применяя к нему некие преобразования. Например, мы можем класть на стек значения такого типа: //тип для заданий в стеке type Traversal. Stack. Record<'a> = | Commit. Action of 'a | Keep. Moving of Tree<'a>

Commit. Action x означает, что мы должны применить функцию action к вершине x. Keep. Commit. Action x означает, что мы должны применить функцию action к вершине x. Keep. Moving tree – продолжить обход для дерева tree: сгенерировать и поместить новые задания на стек. Начальное состояние стека для симметричного обхода: Keep. Moving left Commit. Action v Keep. Moving right

//итеративный симметричный обход let inorder. Iter tree action = let stack = new Stack<_>() //итеративный симметричный обход let inorder. Iter tree action = let stack = new Stack<_>() //: Traversal. Stack. Record<'a> //создаём "заказ". записываем действия в обратном порядке let create. Record = function | Empty -> () | Node(v, left, right) -> stack. Push (Keep. Moving right) stack. Push (Commit. Action v) stack. Push (Keep. Moving left) let process. Record = function | Commit. Action v -> action v | Keep. Moving tree -> create. Record tree while (stack. Count > 0) do process. Record <| stack. Pop()

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

let inorder. Iter. Functional tree action = let stack = new Stack<_>() //: (unit let inorder. Iter. Functional tree action = let stack = new Stack<_>() //: (unit -> unit) //создаём "заказ". записываем действия в обратном порядке let rec create. Record = function | Empty -> () | Node(v, left, right) -> stack. Push <| (fun () -> create. Record right) stack. Push <| (fun () -> action v) stack. Push <| (fun () -> create. Record left) create. Record tree while (stack. Count > 0) do stack. Pop() <| () Задание: напишите аналогичные ф-ции для pre- и postorder.

continuation-passing style, ленивые вычисления continuation-passing style, ленивые вычисления