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

Lektsii_TRPO / 2_Recurs_Lec_2

.doc
Скачиваний:
52
Добавлен:
12.03.2015
Размер:
1.54 Mб
Скачать

12

2.3. Примеры рекурсивных программ.

Отображение более сложных рекурсивных связей в алгоритмах покажем на примерах.

Вычисление корня уравнения методом деления отрезка пополам. Напишем функцию, отыскивающую решение уравнения F(x)=0 на заданном отрезке (А,В) (рис.2.3).

В рекурсивной функции Bisect отрезок (x1,x2) делится пополам. Если значение функции F(x) меньше заданной точности eps вычисления корня, то считается, что корень найден и следует выход, иначе необходимо выбрать один из отрезков (x1,m) или (m,x2) для дальнейшего уточнения корня. Из рис. 2.3 видно, что должен выбираться тот отрезок, на концах которого функция F(x) имеет разные знаки (на рисунке это отрезок (x1,m)). Выбору отрезка соответствует коррекция x1 или x2. Далее функция Bisect вызывается с новыми значениями x1 и x2.

Листинг 2.1. Решение уравнения методом деления отрезка пополам

Procedure Bisect ( Var x1,x2 :Real );

Function F( x :Real) :Real; Begin F := x*x - 2; End;

Begin

m := (x1 + x2)/2.0;

If Abs(F(x2) – F(x1)) < eps Then Exit;

If F(m)*F(x1) < 0

Then x2:=m Else x1:=m;

Bisect ( x1, x2 );

End;

Вызов: ….. x1 := 0; x2:=5; Bisect(x1, x2); Вывод m; …….

Задача о разрезании прямоугольника. Из листа клетчатой бумаги размером n∙m клеток вырезали образующие некоторые фигуры клетки. Линии разреза совпадают с границами клеток. Определить количество вырезанных фигур.

Матрицей А размерности n∙m моделиру­ется лист клетчатой бумаги, при этом

1, если клетка вырезана;

A[i,j] =

0, если не вырезана;

Задание исходных данных — формирова­ние матрицы А — осуществляется с помо­щью генератора случайных чисел. Суть решения заключается в просмотре матрицы А до первой единицы. Как только встречаем единицу, то это означает, что мы вышли на очередную вырезанную фигуру. Поэтому следует изменить значе­ние счетчика числа вырезанных фигур cq и "погасить" все соседние единицы.

Рассмотрим на примере механизм «гашения» соседних единиц. На рис.2.4 показано, что вырезано три фигуры. Встретив первую "1" в клетке [1,2], мы должны просмотреть всех ее соседей во всех направлениях так, как это показано на рисунке. При анализе значений соседних клеток, во-первых, необходимо проверить, не вышли ли значения i и j за допусти­мые пределы, для i от 1 до n, для j от 1 до m, если "да", то проверку соседей в этом направлении следует прекратить и, во-вторых, если в соседней клетке записан 0, то проверку в этом направлении также следует прекратить. Итак, встретив 1 в клетке [1,2], мы просматри­ваем клетки в направлении (i-1) - выход за пре­делы листа, в направлении (i+1) - клетка не отно­сится к вырезанной фигуре (А[2,2]=0), в направ­лении (j-1) - клетка не относится к вырезанной фигуре (А[1,1]=0), в направлении (j+1) - клетка относится к вырезанной фигуре (A[1,3]=1). В этом случае мы должны присвоить А[1,3] значение 0 и рассмотреть клетку А[1,3] как начальную для га­шения единиц, соседних уже этой клетке. Рекурсивная процедура для реализации этого алгоритма приведена ниже.

Листинг 2.2. {Задача о разрезании прямоугольника}

Procedure Clr ( i, j :Integer); {гашение соседней клетки}

Begin

If ( i In [1..n]) And ( j In [1..m] ) And (A[i, j] = 1 ) Then Begin

A[i, j] := 0;

Clr ( i - 1, j );

Clr ( i+1, j );

Clr ( i, j – 1);

Clr ( i, j + 1);

End;

End;

Begin { Головная программа }

Вызов процедуры формирования матрицы А;

cq := 0; {начальное значение счетчика фигур}

For i := 1 To n Do

For j :=1 To m Do

If A[i, j] <> 0

Then Begin Inc(cq); Clr(i, j); End;

Wrteln ( 'Kоличество фигур = ', cq );

End.

Раскраска области изображения. Дано изображение состоящее из одной или нескольких связных однотонных областей Oi и требуется разметить (раскрасить) каждую область некоторым заданным цветом с. Для простоты будем считать, что вначале все области имеют цвет с0. Растровое изображение задано на экране. Приведенный ниже алгоритм просматривает соседние с текущей точкой растра и размечает их цветом с. Переход к соседним точкам растра происходит рекурсивно. Процесс раскраски начинается из некоторой начальной точки (xn,yn) и заполняет все пространство связной области Oi . Каждая точка растра окружена четырьмя соседними точками, координаты которых отличаются от центральной в соответствии с таблицей смещений z (см. пример ниже). Первая строка таблицы z соответствует смещениям по x, а вторая – смещениям по y.

{актуально для некоторых вариантов курсовой работы}

Листинг 2.3. { Раскраска области изображения }

Const {таблица смещений по x и y до соседних точек}

z :Array[1..2, 0..3] Of Integer =

{∆x } ((-1, 0, 1, 0, ),

{∆y } ( 0, -1, 0, 1 ));

Procedure FillRec ( x, y :Integer);

Var i :Integer;

Begin

PutPixel (x, y, c); {раскраска точки (x,y)}

For i:=0 To 3 Do {цикл просмотра 4-х соседних точек}

If GetPixel(x + z[1,i], y + z[2,i]) = c0 {если точка не раскрашена

Then FillRec(x + z[1,i], y + z[2,i]); переходим к ней}

End;

Замечание. Рекурсивный алгоритм раскраски можно представить в двух вариантах: "посмотрел-шагнул" и "шагнул-посмотрел". Другими словами, в первом случае мы сначала выбираем подходящее место для шага вперед и только потом делаем этот шаг (что очень хорошо сообразуется с правилами передвижения, скажем, по болоту). Во втором же случае мы сначала делаем шаг вперед и только потом проверяем, что же именно оказалось у нас под ногами. Все-таки "ходим" мы не по болоту и в любой момент можем "спастись" из неправильно выбранного пикселя.

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

Рекурсивная процедура Go на входе имеет текущие координаты (x,y) шарика. По этим координатам отмечаем положение шарика символом '●'. Далее проверяем, достиг ли шарик последней строки матрицы-лабиринта, если да – то лабиринт пройден и признак прохождения лабиринта p=true. Иначе исследуется окружение шарика: если внизу пусто, то перемещаем шарик вниз и возвращаемся из процедуры; если шарик может катиться по горизонтали, то вначале делается попытка переместить его вправо, а затем влево. Каждое перемещение шарика реализуется рекурсивным вызовом процедуры Go.

Листинг 2.4. { Шарик в лабиринте }

Const n=8; m=16;

Var a :Array [1..n] Of String[m]; {лабиринт}

p :Boolean; {признак достижения конца лабиринта ( вначале p=False)}

Procedure Go (y, x :Byte); {процедура поиска пути в лабиринте}

Begin {x,y – текущие координаты шарика}

a[y,x]:='●';

If y = n Then Begin p:=True; Exit; End;

If a[ y+1, x ] = ' ' Then Begin Go (y + 1, x ); Exit; End;

If ( x < m ) And ( a[ y, x + 1] = ' ' ) Then Go ( y, x + 1 );

If ( x > 1 ) And (a[ y, x 1] = ' ' ) Then Go ( y, x 1 );

End;

Задача о Ханойских башнях. Даны три столбика – А, В, С. На столбике А один на другом находятся четыре диска разного диаметра, пронумерованные сверху вниз. Причем они располага­ются так, что каждый меньший диск находится на большем. Требуется перемес­тить эти четыре диска на столбик С, сохранив их взаиморасположение. Столбик В раз­решается использо­вать как вспомогатель­ный. При решении за один шаг допускается переме­щать только один из верхних дисков ка­кого-либо столбика. Кроме того, больший диск никогда не разре­шается класть на диск меньшего диаметра.

Для определения подхода к решению поставленной задачи, рассмотрим слу­чай с тремя дисками. Рассуждения удобно иллюстрировать с помощью так называе­мого дерева решений (рис.2.6). Дейст­вительно, задача решается, если раз­бить ее на две подзадачи: переложить диски из А в B, а затем переложить их из B в C. Каждую из этих подзадач можно снова раз­бить на две подзадачи, как это показано на рисунке. Решение формулируется как рекурсивный обход дерева по правилу:

1. Обойти левое поддерево.

2. Попасть в корень.

3. Обойти правое поддерево.

Отсюда получается следующий порядок перестановки дисков:

AС AВ СВ AС ВA ВС AС

Далее рассмотрим более общий случай с n дисками. Если мы сможем сформулировать решение для n дисков в терминах решения для n-1 диска, то поставленная проблема была бы решена, поскольку задачу для n-1 диска можно будет, в свою очередь, решить в терминах n-2 дисков и так далее до тривиального случая одного диска. Для случая одного диска (n=1) решение получается элементарным. Зада­чу перемещения n наименьших дисков со стержня А на стержень С можно представить себе состоящей из двух подзадач размера n - 1. Сначала нужно переместить n - 1 наи­меньших дисков со стержня А на стержень B, оставив на стержне А n-й наибольший диск. Затем этот диск нужно переместить с А на C. Потом следует переместить n - 1 дисков со стержня B на стержень C. Это перемещение n - 1 дисков выполняется путем рекурсивного применения указанного метода. Поскольку диски, участвующие в пере­мещениях, по размеру меньше тех, которые в перемещении не участвуют, не нужно задумываться над тем, что находится под перемещаемыми дисками на стержнях А, В или С. Хотя фактическое перемещение отдельных дисков не столь очевидно, а модели­рование вручную выполнить непросто из-за образования стеков рекурсивных вызо­вов, с концептуальной точки зрения этот алгоритм все же довольно прост для пони­мания и доказательства его правильности (а если говорить о быстроте разработки, то ему вообще нет равных). Именно легкость разработки алгоритмов по методу деком­позиции обусловила огромную популярность этого метода; к тому же, во многих слу­чаях эти алгоритмы оказываются более эффективными, чем алгоритмы, разработан­ные традиционными методами.

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

If n=l

Then

Переместить этот единственный диск со столбика А на столбик С и

остановиться.

Else

1. Переместить верхние n-1 диск со столбика А на

столбик В, используя столбик С как вспомогательный.

2. Переместить оставшийся нижний диск со столбика А

на столбик С.

3. Переместить n-1 диск со столбика В на столбик С,

используя столбик А как вспомогательный.

Приведем решение поставленной задачи с помощью рекурсивной процедуры.

{ Пусть n - число дисков на столбике S }

{ S - исходный столбик }

{ D - столбик, на который нужно переставить диски } { T - вспомогательный столбик }

Листинг 2.5. { Задача о Ханойских башнях }

Procedure Move ( n : Byte; S, D, T : Char ); Begin

If n = 1

Then Writeln (' диск 1' ,S,'→', D )

Else Begin

Move ( n-1, S, T, D ); { 1 }

Writeln (' диск ',n:1,S,'→',D )

Move ( n-1, T, D, S); { 2 }

End;

End;

{1} - переставляем n-1 верхних дисков с исходного столбика на

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

{2}- переставляем все п-1 дисков, расположенные на вспомогательном

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

Вызов: Begin ….. Move (n, 'А', 'С, 'В'); ….. End.

Результат работы программы для 4 исходных дисков на столбике А будет таким:

1

2

3

4

5

6

7

8

диск 1 А → В

диск 2 А → С

диск 1 В → С

диск 3 А → В

диск 1 С → А

диск 2 С → В

диск 1 А → В

диск 4 А → С

9

10

11

12

13

14

15

диск 1 В → С

диск 2 В → А

диск 1 С → А

диск 3 В → С

диск 1 А → В

диск 2 А → С

диск 1 В → С

Быстрая сортировка. Сортировка методом простого обмена (методом "пузырька") является в среднем самой неэффек­тивной. Это обусловлено самой идеей метода, ко­торая требует в процессе сортировки сравнивать и

i→ ←j

38

8

16

79

58

48

4

27

действие

27

38

обмен

8

38

i:= i+1

16

38

i:=i+1

79

38

i:=i+1

38

79

обмен

38

4

j:=j-1

4

38

обмен

58

38

i:=i+1

38

58

обмен

38

48

j:=j-1

(27

8

16

4)

38

(48

58

79)

результат

Рис. 2.7. Быстрая сортировка.

обменивать между собой только соседние элемен­ты. Можно существенно улучшить метод сорти­ровки, основанный на обмене. Это улучшение приводит к самому лучшему на сегодняшний день методу сортировки массивов, который можно на­звать обменной сортировкой с разделением. Он основан на сравнениях и обменах элементов, стоящих на возможно больших расстояниях друг от друга. Предложил этот метод Ч. Э. Р. Хоар в 1962 году. Поскольку производительность этого метода просто впечатляюща, автор назвал его QUICKSORT ("быстрой сортировкой").

В этом алгоритме [8] используется результат каждого сравнения, чтобы опре­делить, какие ключи сравнивать следующими. Итак, рассмотрим следующую схему сравнений/обменов. Имеются два указателя i и j, причем вначале i=1, a j = n. Сравним a[i] и a[j] и если обмен не требуется, то уменьшим j на единицу и повторим этот процесс. После первого обмена увели­чим i на единицу и будем продолжать сравнения, увеличивая i, пока не произойдет еще один обмен. Тогда опять уменьшим j и т. д.; будем "сжигать свечку с обоих концов", пока не станет i = j. Посмотрим, например, что произойдет с массивом из восьми чисел (рис. 2.7). Заметим, что в каждом сравнении этого примера участвует ключ х=38; вообще в каждом сравнении будет присутствовать исходный ключ 38, потому что он будет продолжать обмениваться местами с другими ключами каждый раз, когда мы переключаем направление. К моменту, когда i = j , исходный ключ 38, займет свою окончательную позицию, потому что, как нетрудно видеть, слева от нее не будет больших ключей, а справа — меньших.

Исходный массив окажется разделен таким образом, что первоначальная задача сведется к двум более про­стым: сортировке массива a[1]..a[i-1] и (независимо) сортировке массива a[i+1]..a[n]. К каждому из этих подмассивов можно при­менить тот же самый метод, что явно указывает на возможность рекурсии. Исходный ключ 38 еще называют барьером; от значения барьера зависит насколько близки по длине образуются два полученных подмассива.

Алгоритм "быстрой" сортировки можно опре­делить как рекурсивную процедуру, параметрами которой являются нижняя и верхняя границы из­менения индексов сортируемой части исходного массива.

Алгоритм QUICKSORT

  1. Возьмем в сортируемом массиве срединный элемент: m:=a[(1+n)div 2];

  2. Будем производить следующие действия:

    1. начав с левого конца массива, будем перебирать его компоненты до тех пор, пока не встретим элемент a[i], больший m;

    2. начав с правого конца массива, будем перебирать его компоненты до тех пор, пока не встретим элемент a[j], меньший m;

    3. поменяем местами элементы a[i] и a[j];

до тех пор, пока не произойдет «схлапывание». В результате массив будет разделен на две части. В левой части окажутся все компоненты, меньшие m, а в правой - большие m.

  1. Теперь применим эти же действия к левой и к правой части массива - рекурсивно.

Листинг 2.6. { Рекурсивный алгоритм QUICKSORT }

Procedure QSortR ( L, R :Integer); { Recursive sorting Hoar,1962}

Var i, j, m :Integer;{ L, R – левая и правая границы сортируемой части массива }

Begin

i := L; j := R;

m := a[ (R + L) Div 2 ]; {m - индекс середины сортируемой части массива }

Repeat {цикл встречного продвижения индексов i и j}

While a[ i ] < m Do Inc ( i ); {пока обмен не требуется, i → вперед}

While a[ j ] > m Do Dec ( j ); {пока обмен не требуется, j → назад}

If i <= j {если требуется обмен,

Then Begin Обмен a[i] a[j]; он производится и индексы

Inc(i); Dec(j); i и j перемещаются навстречу}

End;

Until i > j; {конец цикла, если произошло «схлапывание» индексов i и j}

If L < j Then QSortR (L, j ); { QSortR от L до места «схлапывания» }

If i < R Then QSortR ( i, R ); { QSortR от места «схлапывания» доR}

End;

Вызов: Begin Ввод массива а; QSortR( 1, n ); Вывод массива а; End.

Замечание. Производительность алгоритма в среднем характеризуется как O(n∙logn ) и, следовательно, относится к улучшенным методам сортировки массивов. Кроме того, даже среди улучшенных методов упорядочения QUICKSORT дает наилучшие результаты по времени (для относительно больших входных последовательностей - начиная с n = 100). Итеративный вариант алгоритма QUICKSORT еще более эффективен. Существует, однако, "плохой" случай (и немногочисленные смежные с ним), когда эффективность QUICKSORT резко ухудшается до n2. Это происходит, если на каждом шаге срединным оказывается такой элемент, что от массива отделяется всего один элемент (массив длины n распадается на два массива длины n-1 и 1). Поэтому при описании эффективности мы использовали слова "в среднем".

Рекурсивная сортировка слиянием. Существует еще один метод сортировки эле­ментов массива, эффективность которого сравни­тельно велика — метод слияния. Этот метод состоит в разбиении данного масси­ва на несколько частей, которые сортируются по отдельности, и в последующем составлении из не­скольких уже упорядоченных массивов искомого упорядоченного массива.

Пусть массив a[1..n] разбивается на части дли­ной m, тогда первая часть — a[1], a[2],..., а[m], вто­рая — а[m+1], a[m+2],..., а[2m]. Если n не делится на m, то в последней части будет менее m элементов. После того как массивы-части упорядочены, можно объединить их в упорядоченные массивы - части, состоящие не более,

6

7

8

2

4

5

3

1

m=1

6

7

2

8

4

5

1

3

m=2

2

6

7

8

1

3

4

5

m=4

1

2

3

4

5

6

7

8

m=8

чем из
Соседние файлы в папке Lektsii_TRPO