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

Метод_материалы / Учебники / Программирование_С

.pdf
Скачиваний:
66
Добавлен:
16.03.2016
Размер:
2.31 Mб
Скачать

}

// функция term, которая распознает термы, похожа на предыдущую

int term (str, pos)

{

ok=factor (str, pos); if (ok == FALSE) return FALSE;

с=getsym(str, pos); if ( c != ‘*’ )

{

pos = pos – 1; return = TRUE;

};

ok=factor(str, pos); if (ok == TRUE) term=TRUE;

else

term = FALSE;

}

Функция factor распознает множители. Она использует дополнительную функцию letter, которая выдает значение TRUE, если ее символьный параметр есть буква, и значение FALSE в противном случае:

int factor (str, pos)

{

c = getsym(str, pos);

if ( c != ’(’ ) //проверка на букву return letter (c);

// данный множитель является некоторым выражением в скобках

ok = express(str, pos); if (ok == FALSE)

return FALSE;

c = getsym(str, pos); if ( c != ‘)’ )

return FALSE; else

return TRUE;

}

171

Заметим, что в каждом из трех алгоритмов (ехргess, term и factor) входная переменная str никогда не модифицируется, так что ее можно сделать глобальной и не передавать как параметр. Входная и выходная переменная pos модифицируется. Все три функции являются рекурсивными, поскольку каждая может косвенно обратиться сама к себе. Предоставляем читателю в качестве примера вручную оттрассировать действия программы для входной строки

«(A*B*C*D) + (F* (F)+G)».

Напишите программу, реализующую данный алгоритм. 3.1.1.1.2. Задача «Ход коня»

Требуется: найти полный обход шахматной доски ходом коня с однократным посещением каждого поля.

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

Пусть конь находится в некоторой клетке доски. Отсюда он может сделать максимум восемь ходов. Из каждой новой позиций — тоже. Прямой перебор всех вариантов для обычной доски 8 x 8 клеток дает астрономическое количество операций.

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

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

Подготовка следующего хода — это вспомогательные операция, которые могут оказаться нужными; выбор следующего возможного хода — из 8 имеющихся вариантов (рис. 3.2, б); ход приемлем, если конь попадает на доску и на свободную клетку; ход нужен, если не вся доска еще заполнена.

Условие // ход приемлем означает, что при первой же успешной попытке программа постарается «развитьуспех», делая следующийход.

Первое представление о решении — псевдокод:

try (// попытка_следующего_хода)

{

//подготовка следующего хода do

//выбор следующего возможного хода if (// ход_приемлем )

//перейти к следующему ходу, если он нужен,

//при неудаче отвергнуть выбранный ход

while ( (// ход неудачен *) and (// есть возможность хода) ); 172

};

Теперь детализируем этот набросок алгоритма еще на один шаг.

try (// попытка следующего хода )

{

// подготовка следующего хода; do

// выбор следующего возможного хода; if (// ход приемлем )

{

// запись хода ;

if (// доска не заполнена )

{

// попытка следующего хода; if (// неудача )

// стереть предыдущий ход ;

}

}

` while ( (// ход неудачен *) and (// есть возможность хода) ); };

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

Структуры данных: очевидно (рис. 3.2), что доска может быть представлена двумерным массивом, последовательность ходов — номером хода в соответствующей позиции, все ходы из текущей позиции — двумя массивами с координатами возможных для коня ходов.

Рис. 3.2. К алгоритму «Тур коня»

Напишите программу, реализующую данный алгоритм. 3.1.1.1.3. Задача «Восемь ферзей»

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

173

является массивом 8x8, который представляет шахматную доску. Элемент board[i, j] равен значению TRUE, если некоторый ферзь находится в позиции (i, j), и значению FALSE в противном случае. Функция good (board) выдает значение TRUE, если никакие два ферзя не нападают друг на друга на шахматной доске, и значение FALSE в противном случае. В конце программы состояние board представляет решение задачи.

main queens()

{

int b;

for( i=1 to 8 ) for (j=1 to 8 )

{

board[i, j] = FALSE; b = try(1);

};

}

int try(n)

{

if ( n>8 )

return TRUE; else

{

for ( i = 1 to 8 )

{

board[n, i] = TRUE;

if (good(board) == TRUE and try(n+i) ==TRUE) try = TRUE;

else

board[n, i] = FALSE;

};

try = FALSE; };

return try;

}

Рекурсивная функция try выдает значение TRUE, если при заданном расположении на шахматной доске board в момент обращения к ней можно добавить какое-либо число ферзей в строках с n-й по 8-ю для того, чтобы получить некоторое решение. Функция try выдает значение FALSE, если нет решения, при котором имеются ферзи в позициях на шахматной доске board, которые уже содержат значение TRUE. Если выдается значение TRUE, то

174

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

Идея, положенная в основу данного решения, состоит в следующем: шахматная доска board представляет глобальную ситуацию во время попытки найти некоторое решение. Следующий шаг в направлении получения некоторого решения выбирается произвольно (ферзь помещается в следующей неопробованной позиции в строке n) и рекурсивно проверяется, можно ли получить некоторое решение, которое включает этот шаг. Если это так, то организуется рекурсивный возврат. Если это не так, то происходит возвращение из попытки сделать следующий шаг (board[n, i] = FALSE) (попытка отвергается) и делается попытка другого возможного шага. Этот метод (используемый также в алгоритме «Тур коня») называется возвращением (backtracking).

3.1.1.1.4. Задача «Ханойские башни»

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

1.На каждом шаге со стержня на стержень переносится точно один диск.

2.Диск нельзя помещать на диск меньшего размера.

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

Рис. 3.3. К алгоритму «Ханойские башни»

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

Рекурсивное решение заключается в том, если мы сможем сформулировать решение для n дисков в терминах n – 1 дисков.

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

175

большой диск с колышка А на колышек С. Таким образом, мы можем установить некоторое рекурсивное решение задачи «Ханойские башни» в следующем виде.

Для того чтобы переместить n дисков с колышка А на колышек С, используя колышек В как вспомогательный, необходимо:

1)если n = 1, переместить единственный диск с колышка А на колышек С и остановиться,

2)переместить верхние n – 1 дисков с колышка А на колышек В, используя колышек С как вспомогательный,

3)переместить оставшийся диск с колышка А на колышек С,

4)переместить n – 1 дисков с колышка В на колышек С, используя колышек А как вспомогательный.

Это решение может быть преобразовано в некоторый алгоритм — назовем его towers.

Какие входные переменные нужны для towers? При рекурсивных вызовах диски будут перемещаться не с колышка А на С, используя колышек В как дополнительный, а с колышка А на В, используя колышек С как промежуточный (шаг 2), или с колышка В на С, используя колышек А (шаг 4). Следовательно, входом в towers должны быть три входные переменные. Первая (source) представляет тот колышек, с которого мы перемещаем диски. Вторая (dest) представляет тот колышек, на который мы будем перемещать диски. И третья (aux) представляет вспомогательный колышек. Итак, задачу решает вызов

void towers (n, source, dest, aux)

{

//первоначально source равен A, dest равен С и aux равен В

//если только один диск, то сделать перемещение и вернуться if (n==1)

printf (“переместить диск 1 с колышка c% на колышек

c%”,source,dest); return 0;

//переместить верхние n – 1 дисков с А на В, используя С как

//вспомогательный колышек

towers(n – 1, source, aux, dest);

//переместить оставшийся диск с А на С

printf (“переместить диск n с колышка c% на колышек c%”,source,dest); //переместить n – 1 дисков с В на С, используя А как вспомогательный //колышек

towers (n – 1, aux, dest, source); return 0;

}

176

Проследим действия вышеприведенного алгоритма, когда в него вводятся значение 4 для переменной n, «А» для source, «С» для dest и «В» для aux. Следует внимательно отслеживать изменяющиеся значения входных переменных source, aux и dest. Проверим, что алгоритм дает правильный вывод:

переместить диск 1 с колышка А на колышек В переместить диск 2 с колышка А на колышек С переместить диск 1 с колышка В на колышек С переместить диск 3 с колышка А на колышек В переместить диск 1 с колышка С на колышек А переместить диск 2 с колышка С на колышек В переместить диск 1 с колышка А на колышек В переместить диск 4 с колышка А на колышек С переместить диск 1 с колышка В на колышек С переместить диск 2 с колышка В на колышек А переместить диск 1 с колышка С на колышек А переместить диск 3 с колышка В на колышек С переместить диск 1 с колышка А на колышек В переместить диск 2 с колышка А на колышек С переместить диск 1 с колышка В на колышек С.

Напишите программу, реализующую данный алгоритм. 3.1.1.1.5. Перемножение натуральных чисел

Другим примером рекурсивного определения является определение умножения натуральных чисел. Произведение а * b, где а и b являются положительными целыми числами, может быть определено как а, прибавленные к самому себе b раз. Это некоторое итерационное определение. Эквивалентное ему рекурсивное определение состоит в следующем:

а*b = а,

если b=1,

a*b=a*(b–1)+а,

если b>1.

Для того чтобы вычислить 6*3 по этому определению, мы должны сначала вычислить 6*2 и затем прибавить 6. Для того чтобы вычислить 6*2, мы должны сначала вычислить 6*1 и добавить 6. Но 6*1 равно 6 согласно первой части данного определения. Таким образом,

6*3 = 6*2 + 6= 6*1 + 6 + 6 = 6 + 6 + 6=18.

Напишите программу, реализующую этот алгоритм. 3.1.1.1.6. Последовательность Фибоначчи

Последовательность Фибоначчи является последовательностью целых чисел

0, 1, 1, 2,3, 5,8, 13,21,34, ...

Каждый элемент в этой последовательности после первых двух элементов является суммой двух предшествующих элементов (например, 0 + 1 = 1, 1 + 1=2, 1 + 2 = 3, 2 + 3 = 5, ...). Если мы положим fib(0)=0, fib(1) = 1 и т. д., то можем

177

определить последовательность Фибоначчи при помощи следующего рекурсивного определения:

fib(n)=n если n = 0 или 1, fib(n)=fib(n – 2) + fib(n – 1) если n>=2.

Для того чтобы вычислить fib(6), например, мы можем применить данное определение рекурсивно и получим

fib (6) = fib (4) +fib (5) = fib (2) +fib (3) +f ib (5)

=fib (0) +fib (1) +f ib (3) +f ib (5)

=0+1+fib(3)+fib(5) = 1+fib(1)+fib(2)+fib(5)

=1 + 1+fib(0)+fib(1)+fib(5)

=2+0+1+fib(5)

=3+fib(3)+fib(4)

=3+fib(1)+fib(2)+fib(4)

=3+1+fib(0)+fib(1)+fib(4) =4+0+1+ fib(2)+ fib(3)

=5+fib(0)+fib(1)+fib(3)

=5+0+1+fib(1)+fib(2)

=6+1+fib(0)+fib(1)=7+0+1=8.

Заметим, что рекурсивное определение чисел Фибоначчи отличается от рекурсивных определений функции вычисления факториала и перемножения. Рекурсивное определение fib ссылается на само себя дважды. Например, fib(6) = fib(4) +fib(5), так что при вычислении fib(6) функция fib должна быть применена рекурсивно дважды. Однако часть вычисления fib(5) включает определение fib(4), так что в реализации данного определения происходят большие избыточные вычисления. В приведенном выше примере fib(3) вычислялось отдельно три раза. Было бы намного более эффективным сохранить значение fib(3) в первый раз, когда оно было вычислено, и повторно его использовать каждый раз, когда это необходимо. Далее приведен итерационный метод вычисления fib(n), который является более эффективным (результат помещается в переменную fib):

if (n <= 1) fib = n;

else

{

lofib = 0; hifib = 1;

for ( i=2 to n )

{

x=lofib;

lofib=hifib; hifib = x+lofib;

};

fib=hifib;

}

178

Этот алгоритм вычисляет все числа Фибоначчи, сохраняя их в переменной hifib. Напишите программу, реализующую данный алгоритм.

3.1.1.1.7. Рекурсивный бинарный поиск Следующий пример иллюстрирует применение рекурсии к поиску.

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

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

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

Приведем псевдокод рекурсивного алгоритма для поиска элемента х между элементами mas(low) и mas(high) в некотором отсортированном массиве mas. Этот алгоритм помещает в переменную binsrch некоторый индекс массива mas, такой что mas(binsrch) = х, если такой индекс существует между low и high. Если х не найден в этой части массива, то в переменную binsrch устанавливается 0. Предполагается, что low больше нуля.

if (low > high)

{

binsrch = 0; return 0;

};

mid = (low+high)/2); if (x == mas[mid])

binsrch=mid; else

179

if (x < mas[mid])

// искать х в диапазоне от mas[low] до mas[mid – 1]; else

//искать х в диапазоне от mas[mid + 1] до mas[high];

Напишите программу, реализующую данный алгоритм.

3.1.1.2. Другие задачи на рекурсию

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

некотором списке целых чисел. Напишите программу. 3.1.1.2.2. Двоичные последовательности

Напишите программу, которая моделирует рекурсивный алгоритм вычисления числа последовательностей n двоичных чисел, которые не содержат подряд двух единиц. (Указание. Вычислите, сколько существует таких последовательностей, которые начинаются с 0, и сколько существует тех, которые начинаются с 1.)

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

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

3.1.1.2.4. Перестановки букв Разработайте рекурсивный метод для печати в алфавитном порядке всех

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

3.1.1.8.5. Наименьший элемент Напишите программу, которая моделирует рекурсивный алгоритм

нахождения k-ro наименьшего элемента в массиве чисел mas при помощи выборки произвольного элемента mas[i] из mas и разделения mas на элементы, которые меньше чем mas[i], равны ему и больше его.

3.1.1.2.6. Лабиринт

Некоторый массив maze из 0 и 1 размером 10x10 представляет лабиринт, в котором путешественник должен найти путь от maze[1,1] до maze[10, 10]. Путешественник может перемещаться из некоторого квадрата в любой соседний квадрат в той же самой строке или том же столбце, но не может перескакивать через какие-либо квадраты или двигаться по диагонали. Кроме того, путешественник не может перемещаться в квадрат, который содержит 1. Элементы maze(1,1) и maze(10, 10) содержат 0. Напишите программу, которая воспринимает некоторый массив maze и печатает или сообщение, что через данный лабиринт проход не существует, или список позиций, представляющий некоторый путь от [1, 1] до [10, 10].

180

Соседние файлы в папке Учебники