
- •Аналіз алгоритмів
- •Алгоритм пошуку рядка
- •[Ред.]Постановка задачі
- •[Ред.]Термінологія
- •[Ред.]Лема про суфікси, що перекриваються
- •[Ред.]Базова класифікація алгоритмів
- •[Ред.]Алгоритми пошуку для одного зразка
- •[Ред.]Алгоритми пошуку скінченної множини зразків
- •[Ред.]Алгоритми пошуку необмеженої множини зразків
- •[Ред.]Інші класифікації
- •[Ред.]Індексуючі методи
- •[Ред.]Інші варіанти
- •Алгоритм сортування
- •[Ред.]Постановка задачі
- •[Ред.]Структури даних
- •[Ред.]Характеристики алгоритмів
- •[Ред.]Теорема про найкращий час сортування
- •[Ред.]Доведення
- •Рекурсивні функції та алгоритми
- •[Ред.]Властивість оптимальної підструктури
- •[Ред.]Коли алгоритми жадібного типу зазнають невдачі
- •[Ред.]Приклади [ред.]Розмін монет
- •[Ред.]Вибір заявок
- •Складність алгоритмів
Рекурсивні функції та алгоритми
Взагалі багато об'єктів різної природи, не тільки функції, визначаються як рекурсивні.
Приклад 1. Множина натуральних чисел.
1 - натуральне число;
Якщо n - натуральне число, то n+1 - натуральне число.
Приклад 2. Двійкові дерева.
o - дерево (пусте, одна изольована вершина).
якщо t1 та t2 - дерева, то
є деревами.
Приклад 3.
|
|
ліва крива Дракона left(S1,S2) порядку 1 |
права крива Дракона right(S1,S2) порядку 1 |
2) Якщо ( S1, S2, ..., S2n-1 ) - крива Дракона порядку n-1, то
( left (S11, S12), right (S21, S22), ..., right (S2n-11, S2n-12) )
- крива Дракона порядку n.
Принципові властивості рекурсивного визначення:
|
У програмуванні рекурсивними можуть бути не тільки функції, але й процедури (з цим ми познайомимося нижче).
При програмуванні рекурсії слід впевнитися у тому, що задачу не можна легше розв'язати за допомогою ітеративних процедур. Рекурсію використовувати варто тоді, коли нерекурсивний підхід сильно ускладнює програмування або не призводить до сильного зменшення часу роботи алгоритму, об'єму пам'яті для роботи тощо.
Приклади. Усі найбільш відомі приклади застосування рекурсії, фактично є прикладом того, як не слід застосовувати рекурсію.
1. n! := ( if n = 0 then 1 else n * (n-1)! );
Програмування цього фрагменту призведе до копіювання у сегмент стеку n рядків сегменту даних процедури обчислення факторіалу та адрес повернення. Простіше написати
f := 1;
if n > 0 then
for i := 1 to n do
f := f*i;
що потребує лише використання параметру циклу.
Ще більш контрастним є наступний приклад.
2. Біноміальні коефіцієнти:
C(n,m) := (if (m = 0) or (m = n)
then 1
else C(n-1,m-1) + C(n-1,m));
Це приведе до ~ 2n викликів процедурою C(n,m) самою себе з меншими параметрами. Між тим, багато викликів будуть дублюватися, буде багато викликів з однаковими параметрами. Навіть застосовуючи саме формулу біноміальної теореми, можна обмежитися лише (m + 1)(n - m + 1) викликів, що значно менше 2n при великих n. Але значно краще використати формулу
|
( 1 ) |
яка програмується за допомогою одного циклу і не призводить до переповнень розрядної сітки.
3. Числа Фібоначчі.
f(0) = 0; f(1) = 1; f(n) = f(n-2) + f(n-1).
Картина повністю аналогічна до такої у прикладі 2. Рекурсивна функція:
function Fibonacci (n: Word): LongInt;
begin
if (n = 0) or (n = 1) then Fibonacci := n;
else
Fibonacci := Fibonacci(n-2) + Fibonacci(n-1)
end;
Попри свою "естетичну витонченість" вимагає 2n викликів, що може привести до переповнення стеку.
Але значно простіше числа Фібоначчі обчислити послідовно:
if (n = 0) or (n = 1)
then f := n
else begin
f1 := 0; f2 := 1;
for i = 1 to n begin
f0 := f1;
f1 := f2;
f2 := f1 + f0;
end;
end;
Поступово зсуваючи значення, обчислюємо числа цього ряду, займаючи лише три комірки пам'яті та не дублюючи викликів з однаковими аргументами.
Не слід використовувати рекурсію, якщо є очевидний ітеративний розв'язок.
Будь-яку рекурсивну-перекурсивну програму врешті-решт ЕОМ перекладає у машинові коди, серед яких рекурсією і не пахне. Це означає, що будь-яку задачу можна розв'язати без застосування рекурсії. Але є випадки, коли застосування рекурсивних алгоритмів є виправданим. Це в першу чергу, область штучного інтелекту.
Приклади, що ми розглянемо далі, розглянуто у Н.Вірта [2,3] та у книзі [4], де всі програми викладено на Фортрані - мовою, що не дозволяє рекурсії.
Вправи.
Напишіть програму для обчислення біноміальних коефіцієнтів
через використання програми-функції для обчислення факторіалу;
без її використання - за допомогою формули (1).
Визначіть область значень аргументів, для яких вона ще працює.
Напишіть програму-функцію, що для даного дійсного аргументу перевіряла б, чи є він натуральним числом. При цьому обов'язково використайте рекурсивне визначення натурального числа.
Контрольні питання.
Що таке рекурсія?
Що таке ітерація?
Чи існують задачі, що неможливо запрограмувати без застосування рекурсії? Наведіть приклади.
РЕКУРСИВНА ПРОГРАМА ПОШУКУ МІНІМУМА
Ясно, що якщо деякий масив поділити на дві частини, відшукати мінімальні елементи у обох частинах, а з них взяти менший, то це і буде мінімальний елемент у масиві. Якщо масив (підмасив) складається з одного елементу, то він і є найменшим.
Найменший елемент підмасиву a[n]..a[m] можна визначити як
Min(a, n, m) = min(Min(a, n, (n+m) / 2), Min(a, (n+m) / 2, m)), якщо n<m; Min(a, n, m) = a[n], якщо n = m.
Тут через min(x, y) позначено менше з x та y, а через Min(a, n, m) - найменший елемент підмасиву a[n .. m].
Відповідна програма на Паскалі має вигляд
type arr = array[0..N-1] of Integer;
var a : arr;
...
function Min(var a: arr; n, m: 0..N-1): Integer;
var
x,y : Integer;
begin
if n > m then Halt;
if n = m then Min := a[n]
else begin
x := Min(a, n, (n+m) div 2]);
y := Min(a, (n+m) div 2 + 1, m);
if x < y then Min := x
else Min := y
end
end;
Цей алгоритм вимагає ~ n звертань до підпрограми, тобто є у порівнянні з попередніми прикладами більш економним. А от навіщо перший параметр - масив задається зі словомvar?
Жа́дібний алгори́тм — простий і прямолінійний евристичний алгоритм, який приймає найкраще рішення, виходячи з наявних на поточному етапі даних, не турбуючись про можливі наслідки, сподіваючись врешті-решт отримати оптимальне рішення. Легкий в реалізації і часто дуже ефективний за часом виконання. Багато задач не можуть бути розв'язані з його допомогою.
Наприклад, використання жадібної стратегії для задачі комівояжерапороджує наступний алгоритм: «На кожному етапі вибирати найближче з невідвіданих міст».
Зміст [сховати]
|
[ред.]Специфіка
Зазвичай, жадібний алгоритм базується на п'яти принципах:
Набір можливих варіантів, з яких робиться вибір;
Функція вибору, за допомогою якої знаходиться найкращий варіант;
Функція придатності, яка визначає придатність отриманого набору;
Функція цілі, оцінює цінність рішення, не виражена явно;
Функція розв'язку, яка вказує на те, що знайдене кінцеве рішення.
[ред.]Визначання придатності
Придатний набір варіантів — такий, що обіцяє не просто отримання рішення, а отримання оптимального рішення задачі.
На відміну від динамічного програмування, що розв'язує проблему знизу догори, жадібна стратегія робить це згори донизу, роблячи один жадібний вибір за іншим, зводячи велику задачу до малої.
[ред.]Критерії застосування
Жадібний алгоритм добре розв'язує деякі задачі, а інші — ні. Більшість задач, для яких він спрацьовує добре, мають дві властивості: по-перше, до них можливо застосувати Принцип жадібного вибору, по-друге, вони мають властивість Оптимальної підструктури.
[ред.]Принцип жадібного вибору
До оптимізаційної задачі можна застосувати принцип жадібного вибору, якщо послідовність локально оптимальних виборів дає глобально оптимальний розв'язок. В типовому випадку доведення оптимальності має таку схему: спочатку доводиться, що жадібний вибір на першому етапі не унеможливлює шлях до оптимального розв'язку: для всякого розв'язку є інше, узгоджене із жадібним і не гірше першого. Далі доводиться, що підзадача, що виникла після жадібного вибору на першому етапі, аналогічна початковій, і міркування закінчується за індукцією. Інакше кажучи, жадібний алгоритм ніколи не переглядає свої попередні вибори для здійснення наступного, на відміну від динамічного програмування.