Использование рекурсии является красивым приёмом программирования. Рекурсивные версии программ, как правило, гораздо короче и нагляднее. В то же время в большинстве практических задач этот приём неэффективен с точки зрения расходования таких ресурсов ЭВМ, как память и время исполнения программы. Использование рекурсии увеличивает время исполнения программы и зачастую требует значительного объёма памяти для хранения копий подпрограммы на рекурсивном спуске. Поэтому на практике разумно заменять рекурсивные алгоритмы на итеративные. А любой рекурсивный алгоритм можно преобразовать в эквивалентный итеративный (то есть использующий циклические конструкции).
Метод перебора с возвратом
Рассмотрим алгоритм перебора с возвратом (backtracking) на примере задачи о прохождении лабиринта.
Дан лабиринт, оказавшись внутри которого нужно найти выход наружу. Перемещаться можно только в горизонтальном и вертикальном направлениях. На рисунке показаны все варианты путей выхода из центральной точки лабиринта.
Для получения программы решения этой задачи нужно решить две проблемы:
•как организовать данные;
•как построить алгоритм.
Информацию о форме лабиринта будем хранить в квадратной матрице LAB символьного типа размером N x N, где N — нечетное число (чтобы была центральная точка). На профиль лабиринта накладывается сетка так, что в каждой ее ячейке находится либо стена, либо проход.
12
Матрица отражает заполнение сетки: элементы, соответствующие проходу, равны пробелу, а стене — какому-нибудь символу (например, букве М).
Путь движения по лабиринту будет отмечаться символами +.
Например, приведенный выше рисунок (в середине) соответствует следующему заполнению матрицы LAB:
М М М М М М М М М М М |
||||||||||
М |
|
+ |
+ |
+ |
|
|
М |
|
|
|
М |
М |
+ |
М |
|
М |
|
М |
|
|
М |
М |
|
+ |
М |
+ |
М |
М |
М |
|
|
М |
М |
М |
+ |
М |
+ |
М |
|
|
|
|
М |
М |
М |
+ |
М |
+ |
+ |
|
М |
М |
М |
М |
М |
М |
+ |
М |
М М М |
М |
М М |
М |
|||
М |
М |
+ |
+ |
+ |
|
|
|
|
|
М |
М |
|
|
М |
+ |
М |
М |
М |
|
М |
М |
М |
М |
М |
М |
+ |
+ |
+ |
+ |
+ |
|
М |
М |
|
|
|
М |
М |
М |
М |
+ |
М |
М |
Исходные данные — профиль лабиринта (исходная матрица LAB без крестиков); результат — все возможные траектории выхода из центральной точки лабиринта (для каждого пути выводится матрица LAB с траекторией, отмеченной крестиками).
Алгоритм перебора с возвратом еще называют методом проб. Суть метода:
1.Из каждой очередной точки траектории просматриваются возможные направления движения в одной и той же последовательности (например, вверх- вниз-вправо-влево); шаг производится в первую же обнаруженную свободную соседнюю клетку; клетка, в которую сделан шаг, отмечается крестиком.
2.Если из очередной клетки дальше пути нет (тупик), то следует возврат на один шаг назад и просматриваются еще не испробованные пути движения из этой точки; при возвращении назад покинутая клетка отмечается пробелом.
3.Если очередная клетка, в которую сделан шаг, оказалась на краю лабиринта (на выходе), то на печать выводится найденный путь.
Программа строится методом последовательной детализации.
Первый этап детализации:
Program Labirint;
13
Const NN=30; {максимальный размер лабиринта NNxNN клеток}
Type Field=Array[1..NN,1..NN] Of Char; Var LAB:Field;
N,M:Byte; {заданный размер лабиринта}
...
Procedure GO(X,Y:Integer); Begin
{Поиск путей из центра лабиринта до края – каждый найденный путь печатается}
End;
Begin
{Ввод лабиринта}
GO(N Div 2, M Div 2) {начинаем с середины} End.
Процедура GO пытается сделать шаг в клетку с координатами х, у. Если эта клетка оказывается на выходе из лабиринта, то пройденный путь выводится на печать. Если нет, то в соответствии с установленной выше последовательностью делается шаг в соседнюю клетку. Если клетка тупиковая, то выполняется шаг назад. Из сказанного выше следует, что процедура носит рекурсивный характер.
Запишем сначала общую схему процедуры без детализации:
Procedure GO(X,Y:Integer); Begin
If {клетка (x,y) свободна} Begin
{шаг на клетку (x,y)}
If {дошли до края лабиринта} Then {печатается найденный путь}
Else {попытка сделать шаг в соседние клетки в условленной последовательности}
{возвращение на один шаг назад} End
End.
Для вывода найденных траекторий составляется процедура PRINTLAB.
В окончательном виде программа будет выглядеть так: 14
Program Labirint;
Const NN = 30; {Максимально возможный размер лабиринта} Type Field = Array[1..NN,1..NN] Of Char;
Var Lab : Field; {лабиринт}
N,M : Byte; {заданный размер лабиринта} I,J : Byte;
S : String;
Procedure PrintLab; {Вывод найденных траекторий} Var I,J : Byte;
Begin
writeln (‘-------------- <Enter>‘); For I:=1 To N Do Begin
For J:=1 To M Do Write(LaB[I,J]);
WriteLn; End; WriteLn; ReadLn
End;
Procedure Go(X,Y: Byte);
Begin |
|
If Lab[X,Y]=' ' Then |
{если клетка свободна} |
Begin |
|
Lab[X,Y]:='+'; |
{делается шаг} |
If (X=1) Or (X=N) Or (Y=1) Or (Y=M) {край} |
|
Then PrintLab |
{печатается путь} |
Else Begin |
{поиск следующего шага} |
Go(X+1,Y); |
|
Go(X-1,Y); |
|
Go(X,Y+1); |
|
Go(X,Y-1); |
|
End; |
|
Lab[X,Y]:=' ' |
{возвращение назад} |
End; |
|
End; |
|
15
Begin
{Ввод лабиринта}
Write('Введите размерность лабиринта по вертикали '); ReadLn(N);
Write('Введите размерность лабиринта по горизонтали '); ReadLn(M);
WriteLn('Введите лабиринт по строкам, стены символом М’, проходы - пробелом');
For I:=1 To N Do Begin ReadLn(S);
For J:=1 To M Do
Lab[I,J]:=S[J] End;
Go(N div 2,M div 2) End.
Схема алгоритма данной программы типична для метода перебора с возвратом. По аналогичным алгоритмам решаются, например, популярные задачи об обходе шахматной доски фигурами или о расстановке фигур на доске так, чтобы они «не били» друг друга; множество задач оптимального выбора (задачи о коммивояжере, о рюкзаке, об оптимальном строительстве дорог и т.п.).
Пример 2.1.. Заданы N различных натуральных чисел. Выбрать из этих чисел такие, чтобы их сумма равнялась заданному числу Z. Вывести все возможные решения.
Например, N=6: 1, ,9, 3, 2, 5, 6. При Z=10:
19
13 6
3 2 5
Const NN = 30;
Type Mass = array[1..NN] Of Word; DopMass = Array[1..NN] Of Char;
Var A : Mass;
D : DopMass;
16