Вы находитесь на странице: 1из 15

АЛГОРИТМ ПЕРЕБОРА С ВОЗВРАТОМ

Поиск с возвратом – это метод перебора всех возможных решений для


нахождения так называемого множества полных решений. Каждое частичное
решение определяет некоторое подмножество полных решений. Метод перебора с
возвратом основан на том, что при поиске частичного решения многократно
делается попытка расширить так называемое текущее частичное решение.
Определение переборной подпрограммы следует понимать следующим
образом: для всех первых ходов, будут перебраны всевозможные вторые ходы,
затем для вторых ходов будут перебраны всевозможные третьи ходы и так далее,
пока не будут перебраны все варианты ходов. Таким образом, ищется первый
подходящий вариант, затем для этого варианта хода второй и так далее, пока для
очередного хода не будут перебраны все варианты. Можно сравнить это с
построением некой цепочки ходов. Поскольку для каждого хода есть несколько
вариантов следующих ходов, образуется своеобразное дерево рекурсивных
вызовов перебора с возвратами. Отсюда следует и оценка времени выполнения
переборных программ - KN, где K - количество вариантов следующих ходов для
данного хода, n - количество ходов, то есть время выполнения программы растет
экспоненциально от количества различных вариантов для данного варианта
Алгоритм перебора с возвратом используется, когда решение представляет
собой некоторую последовательность элементов: a1 , a 2 ,..., a n . Суть его
заключается в следующем. На каждом этапе имеется некоторое частичное
решение a1 , a2 ,..., a k , к которому мы пытаемся добавить следующий элемент из
множества возможных (происходит перебор возможных продолжений). Процесс
добавления элементов завершается либо когда получено решение, либо когда не
остается элементов, которые можно добавить. Если элементов для добавления
нет, а решение не получено, то последний добавленный элемент удаляется
(осуществляется возврат к предыдущему шагу), и предпринимается попытка
добавить другой элемент. Алгоритм заканчивается если найдено решение либо
все варианты исчерпаны.

1
Классическими задачами, на которых обычно демонстрируется этот подход,
являются обход конем доски размером N×N, расстановка N ферзей на доске N×N
и задача коммивояжера.
Решаемые методом перебора с возвратом задачи, как правило, принадлежат
одному из трех классов – требуется либо найти произвольное из решений, либо
перечислить все возможные решения, либо найти решение, оптимальное по
заданному критерию.
Пусть глобальная логическая переменная Success получает значение True
как только будет найдено решение. До начала работы алгоритма она должна быть
инициализирована в False. Пусть также i – переменная, характеризующая
глубину рекурсии и передаваемая как параметр рекурсивной процедуре.
Алгоритм перебора с возвратом для нахождения одного решения
procedure Back1(i:integer);
begin
формирование списка кандидатов
repeat
выбор очередного кандидата (перебор)
if подходит then
begin
запись кандидата
if решение неполное then
begin
Back1(i+1);
if not Success then
стирание кандидата (возврат)
end;
else Success:=True
end;
until Success or кандидатов больше нет
end;
2
Отметим, что как только переменная Success получает значение True, мы
выходим из всех рекурсивных вызовов процедуры Back1, не совершая никаких
дополнительных действий.

Пример: Рассмотрим реализацию указанного выше алгоритма на примере


задачи обхода конем шахматной доски размера n × n . Обход, если он существует,
можно выполнить за n2-1 шагов.

Доска представляется в виде двумерного массива n×n. Элементами массива


будут номера ходов коня. Значение 0 означает, что клетка еще не посещалась,
значение равное k означает, что в клетку попали на k-ом шаге.

Опишем функцию, осуществляющую обход доски конем. Функция имеет три


параметра: i – номер шага, x и y – текущее положение коня, т.е. координаты
клетки, в которой располагается конь. В результате выполнения функции
выдается логическое значение true, если с заданной позиции удалось обойти
доску полностью, и false в противном случае.

Требуется продолжать обход доски, если верно i<n2. По правилам хода коня
из клетки с координатами (x, y) можно сделать 8 ходов и получить текущее
положение с координатами (u, v).

Пронумеруем ходы так, как показано на рисунке.

Чтобы получить очередной ход, следует изменить текущие координаты


расположения коня на значения, хранящиеся в массивах dx и dy. Следующий ход
коня будет: u:=x+dx[k]; v:=y+dy[k].
3
Для каждого из этих ходов необходимо проверить, находится ли клетка в
пределах шахматной доски, и не ходил ли конь в эту клетку раньше. Если клетка
– в пределах доски и конь в нее еще не ходил, то мы пробуем совершить в нее
ход, помечая клетку номером хода, после чего, если все клетки обойдены, то мы
устанавливаем переменную Success в True (решение найдено), в противном
случае вызываем функцию Back1 повторно с координатами новой клетки в
качестве параметра и проверяем, решена ли задача. Если задача не решена, то ход
следует признать неудачным и стереть его из списка ходов (возврат).

Процедура init осуществляет формирование массива для получения ходов


коня и заполнения клеток таблицы Solution нулевыми значениями.
program KnightWay;
const
n=8;
var
x0,y0,i,j: integer;
dx: array [1..8] of integer;
dy: array [1..8] of integer;
Solution: array [1..n,1..n] of integer;// массив
ходов
Procedure Init;
Begin dx[1]:=2; dy[1]:=1; dx[2]:=1; dy[2]:=2;
dx[3]:=-1; dy[3]:=2; dx[4]:=-2; dy[4]:=1;
dx[5]:=-2; dy[5]:=-1; dx[6]:=-1; dy[6]:=-2;
dx[7]:=1; dy[7]:=-2; dx[8]:=2; dy[8]:=-1;
for i:=1 to n do
for j:=1 to n do
Solution[i,j]:=0;
End;

4
function Back1(i,x,y: integer):boolean;
var k, // номер кандидата или число возможных вариантов ходов
коня
u,v: integer; // координаты следующего хода коня
Success: boolean; //результат обхода после выполнения
очередного шага
begin
k:=0; Success:=False;
repeat
k:=k+1; // номер варианта для хода коня
u:=x+dx[k];
v:=y+dy[k];//вычисление координат следующего хода коня
if (u>0) and (u<=n) and (v>0) and (v<=n)
then //в пределах доски
If Solution[u,v]=0
then //клетку не посещали или она свободна
begin
Solution[u,v]:=i;
if i<n*n then
//есть не пройденные клетки
begin//обход доски со следующей позиции коня
Success:= Back1(i+1,u,v);
if not Success then //возврат на
предыдущий шаг
Solution[u,v]:=0;//клетка объявляется
свободной
end
else
Success:=True//всю доску обошли
end
until Success or (k=8);

5
Back1:= Success;
end;
begin // основная программа
read(x0,y0);
Solution[x0,y0]:=1;
if Back1(2,x0,y0) then
for i:=1 to n do // вывод массива ходов
begin
for j:=1 to n do
Write(Solution[i,j],' ');
writeln
end
else writeln('net resenia');
end.
Если размер доски 5×5, а начало обхода осуществляется с клетки с
координатами (n,n), то в результате вызова функции Back1(2,5,5) будет
сформирована матрица, содержащая номера ходов коня.

Алгоритм перебора с возвратом для нахождения всех решений


Алгоритм построения всех решений проще предыдущего, поскольку в нем
отсутствует флаг Success и после нахождения решения оно сразу же
обрабатывается (например, выводится на экран), и поиск оставшихся решений
продолжается.

6
procedure Backall(i: integer);
begin
Формирование списка кандидатов
repeat
выбор очередного кандидата
if подходит then
begin
запись кандидата
if решение неполное then
Backall(i+1)
else печать решения (или его обработка)
стирание кандидата
end
until кандидатов больше нет
end;

Пример: Задача о n ферзях. На шахматной доске размера n×n расставить n


ферзей так, чтобы они не били друг друга. Эту задачу решил больше 200 лет тому
назад великий математик Леонард Эйлер. Очевидно, на каждой из 8 вертикалей
должно стоять по ферзю.
X
X
X
X
X

Решение:

диагональ 1-го типа и диагональ 2-го типа:

7
var
{признак занятости диагоналей первого типа }
up: array[2 .. 16] of boolean;

{признак занятости диагоналей второго типа }


down: array[-7 .. 7] of boolean;

{признак занятости вертикали }


vert: array[1 .. 8]of boolean;

{номер вертикали, на которой стоит ферзь на каждой


горизонтали }
ihor: array[1 .. 8]of integer;
n: integer;

{проверка на допустимость хода в позицию (i,j)}


function d_hod(i, j: integer): boolean;
begin
d_hod := vert[j] and up[i+j] and down[i-j];
end;

procedure hod(i, j: integer); { сделать ход }


begin
ihor[i] := j;
vert[j] := false;

8
up[i+j] := false;
down[i-j] := false;
end;

procedure o_hod(i, j: integer); { отменить ход }


begin
vert[j] := true;
up[i+j] := true;
down[i-j] := true;
end;

Нахождение всех решений:


procedure print;
var i: integer;
begin
write(' ',s,' ');
for i:=1 to n do write(ihor[i],' ');
writeln;
end;
procedure find_all(i: integer);
var j: integer;
begin
if i<=n then begin
for j:=1 to n do
if d_hod(i,j) then begin
hod(i,j);
find_all(i+1);
o_hod(i,j);
end;
end

9
else begin
inc(s);
print;
end;
end;
Общая схема перебора всех возможных решений, где X1 , X2 , ... , Xn –
представляет собой пространство перебора.
procedure перебор_с_возвратом(i : integer);
var k : integer;
begin
if i > n then
<найдено решение - печать>
else
for k:=1 to nr[i] do begin
x[i]:= k;
if <x[i] совместимо с x[1],...,x[i-1]> then
перебор_с_возвратом(i+1);
end;
end;

Пример: Задача о лабиринте


Дано клеточное поле, часть клеток занята препятствиями. Необходимо
попасть из некоторой заданной клетки в другую заданную клетку путем
последовательного перемещения по клеткам.
Классический перебор выполняется по правилам:
• в каждой клетке выбирается еще не исследованный путь;
• если из исследуемой в данный момент клетки нет путей, то
возвращаемся на один шаг назад (в предыдущую клетку) и пытаемся
выбрать другой путь.

10
Пусть лабиринт представлен матрицей L, состоящей из n строк и m колонок,
со значениями элементов 0 (проход) и 1 (стена/барьер). На позиции (xb,yb)
расположен кусок сыра, на позиции (xs,ys) – мышка. Необходимо вывести все
возможные достижимые перемещения мышки к сыру, зная, что она может
перемещаться только через свободные проходы в направлениях С, СВ, В, ЮВ, Ю,
ЮЗ, З, СЗ.
Например, для лабиринта 4×4 с позицией мыши (1,1) и сыра (4,4)
перемещения м.б.

0 1 1 1
0 1 1 1
0 1 0 0
1 0 1 0

Решение 1: Решение 2:
* 1 1 1 * 1 1 1
* 1 1 1 * 1 1 1
* 1 * * * 1 * 0
1 * 1 * 1 * 1 *

Для того, чтобы мышка не проходила по несколько раз одни и те же


позиции, исключая риск петли, обозначим в лабиринте пройденные позиции
номером шага k.
Массивы Dx и Dy описывают смещения по x и по y при ходе в
соответствующем направлении, инициализируем их как строка и колонка, для
всех 8 возможных перемещений.

11
Чтобы не проверять постоянно, если мышка не дошла каким-то образом до
границы лабиринта, обнесем лабиринт стеной (соответственно две строки и
колонки определим значением 1).
Условия:
Из позиции (x,y) мышка может переместиться в направлении dir, т.е. на
позицию (x+Dx[dir],y+Dy[dir]), только если
L[x+Dx[dir],y+Dx[dir]]=0
(эта позиция является проходом, через который сможет пройти мышка).

program Labirint;

const

NMax = 20; {максимальная размерность лабиринта}

Dx: array[1..8] of integer = (-1,-1,0,1,1,1,0,-1);

Dy: array[1..8] of integer = (0,1, 1,1,0,-1,-1,-1);

type

Index = 0 .. NMax +1;

Labirint = array[Index, Index] of integer;

var L: Labirint;

n, m, xs, ys, xb, yb: Index;

k,NRes: word;

procedure Init;

var i, j: Index;

begin

readln(n,m);

readln(xs, ys, xb, yb);

12
for i:=1 to n do

for j:=1 to m do

read(L[i,j]) ;

end;

procedure Stena; {обнесем лабиринт стеной-барьером}

var i: Index;

begin

for i := 0 to n+1 do {стена слева и справа}

begin L[i,0]:= 1; L[i,m+1]:= 1 end;

for i := 0 to m +1 do {стена сверху и снизу}

begin L[0,i]:= 1; L[n+1,i]:= 1 end

end;

function Final(x,y: Index): boolean;

{возвращает true, если на позиции (x,y) есть сыр}

begin

Final := false;

if (x = xb) and (y = yb) then Final:= true

end;

procedure Vivod;

var i,j: Index;

begin

inc(Nres);

13
writeln(Решение №', NRes);

for i:= 1 to n do begin

for j:= 1 to m do

write(L[i,j]);

writeln

end;

end;

procedure Poisk(x,y: Index; k:word);

var dir: 1..8;

begin

L[x,y]:= k; {ставим метку для позиции x y}

if Final(x,y) then

Vivod

else

for dir:=1 to 8 do

if L[x+Dx[dir],y+Dy[dir]]=0 then {непройденная


позиция}

Poisk(x+Dx[dir], y+Dy[dir], k+1);

L[x,y]:=0; {при возврате снимаем метку, что


посетить данный проход (позицию) и при других вариантах}

end;

begin {основная программа}

Init; Stena;
14
Poisk(xs,ys,1);

if NRes=0 then writeln('Нет решений!');

end.

15