- •Глава 2. Перебор и методы его сокращения
- •2.1. Перебор с возвратом
- •2.1.1. Общая схема
- •2.1.2. Задача о расстановке ферзей
- •2.1.3. Задача о шахматном коне
- •2.1.4. Задача о лабиринте
- •2.1.5. Задача о парламенте
- •2.1.6. Задача о рюкзаке (перебор вариантов)
- •2.1.7. Задача о коммивояжере (перебор вариантов)
- •2.1.8. Задача о секторах
- •Входные и выходные данные
- •2.2. Динамическое программирование
- •2.2.1. Задача о Черепашке
- •2.2.2. Треугольник
- •2.2.3 Степень числа
- •2.2.4. Автозаправка
- •2.2.5. Алгоритм Нудельмана-Вунша
- •2.2.6. Разбиение выпуклого n-угольника
- •2.2.7. Задача о рюкзаке (динамическая схема)
- •2.2.8. Задача о паркете
- •2.2.9. «Канадские авиалинии»
- •2.3. Метод «решета»
- •2.3.1. Решето Эратосфена
- •2.3.2. Быки и коровы
- •2.4. Задачи
2.2.9. «Канадские авиалинии»
Вы победили в соревновании, организованном Канадскими авиалиниями. Приз - бесплатное путешествие по Канаде. Путешествие начинается с самого западного города, в который летают самолеты, проходит с запада на восток, пока не достигнет самого восточного города, в который летают самолеты. Затем путешествие продолжается обратно с востока на запад, пока не достигнет начального города. Ни один из городов нельзя посещать более одного раза за исключением начального города, который надо посетить ровно дважды ( в начале и в конце путешествия). Вам также нельзя пользоваться авиалиниями других компаний или другими способами передвижения. Задача состоит в следующем: дан список городов и список прямых рейсов между парами городов; найти маршрут, включающий максимальное количество городов и удовлетворяющий вышеназванным условиям.
Сделаем первоначальное упрощение задачи. Пусть нам необходимо попасть из самого западного города в самый восточный, посетив при этом максимальное количество городов. Связи между городами будем записывать с помощью массива West:
Пусть мы каким-то образом решили задачу для всех городов, которые находятся западнее города с номером i, т.е. для городов с номерами 1..(i-1). Что значит решили? У нас есть маршруты для каждого из этих городов, и нам известно, через сколько городов они проходят. Обозначим через множество городов западнее i и связанных с городом i авиалиниями. Для этих городов задача решена, т.е. известны значения d[j] (j) - количество городов в наилучшем маршруте, заканчивающемся в городе с номером j.
Итак:
d[i]:=0;
for jQido
if d[j]+1>d[i] then d[i]:=d[j]+1;
Остается открытым вопрос, а где же маршрут? Ибо значение d дает только количество городов, через которые он проходит. А для этого необходимо запомнить не только значение d[i], но и номер города j, дающего это значение. Возможно использование следующей структуры данных:
A:array[1..max] of record
d, l:byte;
end;
И реализация сказанного имеет вид:
FillChar(A,SizeOf(A),0);
A[1].d:=1;
for i:=2 to n do begin
for j:=1 to i-1 do if west[j,i] then if A[j].d+1>A[i].d then begin
A[i].d:=A[j].d+1;
A[i].l:=j;
end;
end;
Видим, что это не что иное, как метод динамического программирования в действии. Осталось выполнить обратный просмотр массива A по значениям поля l, начиная с элемента A[n].l и вывести из массива name:array[1..max] of string[15] названия городов.
Этот вывод можно осуществить с помощью следующей простой рекурсивной процедуры.
procedure rec(i:byte);
begin
if i<>1 then begin rec(A[i].l); writeln(name[i]);end;
end;
Как обобщить этот набросок решения для первоначальной формулировки задачи? Для удобства перенумеруем города в обратном порядке - с востока на запад. Изменим массив A:
A:array[1..max,1..max] of record
d,l:byte;
end;
Под элементом A[i,j].d- путь из городаi в городj - понимается максимальное число городов в маршруте, состоящем из двух частей: отiдо 1 (на восток) и от 1 доj(на запад). По условию задачи нам необходимо найти наилучший по числу городов путь изn-го города вn-й. Считаем, чтоA[1,1].d=1иA[i,j]=A[j,i]. ЧерезQi обозначим множество городов, из которых можно попасть в городi.Верны следующие соотношения:
A[i,j].d=max(A[k,j].d+1), приkÎQi,k<>j, еслиj<>1;
A[i,i].d=max(A[k,i].d), приi>1 иkÎQi.
Рассмотрим логику формирования элементов массиваAна следующем примере.
i |
j |
Qi |
k |
Действия |
2 |
1 |
[1] |
1 |
A[2,1].d:=A[1,1].d+1=2 A[2,1].l:=1 |
2 |
2 |
- |
- |
- |
3 |
1 |
[1] |
1 |
A[3,1].d:=A[1,1].d+1=2 A[3,1].l:=1 |
3 |
2 |
[1] |
1 |
A[3,2].d:=A[1,2].d+1=3 A[3,2].l:=1 |
3 |
3 |
- |
- |
- |
4 |
1 |
[3] |
3 |
A[4,1].d:=A[3,1].d+1=3 A[4,1].l:=3 |
4 |
2 |
[3] |
3 |
A[4,2].d:=A[3,2].d+1=4 A[4,2].l:=3 |
4 |
3 |
[3] |
3 |
- |
4 |
4 |
- |
- |
- |
5 |
1 |
[2,3,4] |
2 |
A[5,1].d:=A[2,1].d+1=3 A[5,1].l:=1 |
5 |
1 |
[2,3,4] |
3 |
A[5,1].d~A[3,1].d+1 |
5 |
1 |
[2,3,4] |
4 |
A[5,1].d:=A[4,1].d+1=4 A[5,1].l:=4 |
5 |
2 |
[2,3,4] |
2 |
- |
5 |
2 |
[2,3,4] |
3 |
A[5,2].d:=A[3,2].d+1=4 A[5,2].l:=3 |
5 |
2 |
[2,3,4] |
4 |
A[5,2].d:=A[4,2].d+1=5 A[5,2].l:=4 |
5 |
3 |
[2,3,4] |
2 |
A[5,3].d:=A[2,3].d+1=4 A[5,3].l:=2 |
5 |
3 |
[2,3,4] |
3 |
- |
5 |
3 |
[2,3,4] |
4 |
A[5,3].d~A[4,3].d+1 |
5 |
4 |
[2,3,4] |
2 |
A[5,4].d:=A[2,4].d+1=5 A[5,4].l:=2 |
5 |
4 |
[2,3,4] |
3 |
A[5,4].d~A[3,4].d+1 |
5 |
4 |
[2,3,4] |
4 |
- |
5 |
5 |
[2,3,4] |
2 |
A[5,5].d:=A[2,5].d+1=6 A[5,5].l:=2 |
5 |
5 |
[2,3,4] |
3 |
A[5,5].d~A[3,5].d+1 |
5 |
5 |
[2,3,4] |
4 |
A[5,5].d~A[4,5].d+1 |
После этой прокрутки массив Aпо полюd имеет вид:
A[i,j].d |
1 |
2 |
3 |
4 |
5 |
1 |
1 |
2 |
2 |
3 |
4 |
2 |
2 |
1 |
3 |
4 |
5 |
3 |
2 |
3 |
1 |
0 |
4 |
4 |
3 |
4 |
0 |
1 |
5 |
5 |
4 |
5 |
4 |
5 |
6 |
а по полю l («*» обозначено неопределенное значение):
A[i,j].l |
1 |
2 |
3 |
4 |
5 |
1 |
* |
1 |
1 |
3 |
4 |
2 |
1 |
1 |
1 |
3 |
4 |
3 |
1 |
1 |
1 |
* |
2 |
4 |
3 |
3 |
* |
3 |
2 |
5 |
4 |
4 |
2 |
2 |
2 |
Попробуем по этой таблице вывести путь для нашего примера с помощью следующей процедуры:
procedure Way(i,j:integer);
begin
if (i=1) and (j=1) then writeln(name[1])
else if i>=j then begin
writeln(name[i]);
Way(A[i,j].l,j);
end
else begin
Way(i,A[i,j].l);
writeln(name[j]);
end;
end;
Получаем цепочку:Way[5,5] ®Way[2,5] ®Way[2,4] ®Way[2,3] ®Way[2,1] ®Way[1,1]. Жирным шрифтом выделены процедуры, в которых имя города выводится на входе в рекурсию, курсивом - на выходе из рекурсии. Цепочка имеет вид: 5 2 1 3 4 5 (индексы городов, напомним, что нумерация изменена для удобства на противоположную).
Осталось привести процедуру нахождения A (ее фрагмент).
...
FillChar(A,SizeOf(A),0);
A[1,1].d:=1;
for i:=2 to n do
for j:=1 to i do
if (i<>j) or (i=n) then
for kÎQi do
if ((k<>j) or (j=1)) and (A[k,j].d<>0) and (A[k,j].d+1>A[i,j].d) then
begin
A[i,j].d+A[k,j].d+1;A[j,i].d:=A[i,j].d;
A[i,j].l:=k;A[j,i].l:=A[i,j].l;
end;
Рассмотрим решения задачи методом полного перебора вариантов. Для этого требуется ряд дополнительных структур данных, а именно:
New:array[1..max] of boolean;
way_r:array[1..2*max] of byte;
Первый массив необходим для хранения признака -посещался или нет город (New[i]=true - города с номером i нет в маршруте). Во втором и третьем массивах запоминаются по принципу стека текущий и наилучший маршруты соответственно. Для работы со стеками необходимы указатели - переменные yk и yk_max. Логика перебора поясняется на нижеприведенном рисунке. Ищем путь из города с номером i.
Фрагмент основной программы.
begin
...
FillChar(New,SizeOf(New),true);
yk:=1;Way[yk]:=1;pp:=true;yk_max:=0;
rec(1);
...
(* Вывод результата *)
...
end.
И процедура поиска решения.
procedure rec(i:byte);
var j,pn,pv:byte;
begin
if i=n then pp:=false; (* меняем направление *)
if (i=1) and not pp then begin
(* вернулись в первый город *)
if yk>yk_max then begin (* запоминаем решение *) yk_max:=yk;
way_r:=way;
end;
pp:=true; (* продолжаем перебор *)
end;
(* по направлению определяем номера просматриваемых городов*)
if pp then begin pn:=i+1; pv:=n end
else begin pn:=1; pv:=i-1 end;
for j:=pn to pv do begin if West[i,j] and New[j] then begin
(* в город с номером j летают самолеты из города i и город j еще не посещался *)
(* включаем город j в маршрут *)
Inc(yk);Way[yk]:=j;New[j]:=false;
rec(j);
(* исключаем город j из маршрута *)
New[j]:=true;Way[yk]:=0;Dec(yk);
(* если исключается самый восточный город (n), то меняем направление *)
if j=n then pp:=true;
end;
end;
Следует отметить, что при этой реализации упрощается вывод результата.
writeln(n);
writeln(yk_max-1);
for i:=1 to yk_max do writeln(name(Way_r[i]]);
Целесообразно сравнение решений при больших значениях n по времени выполнения.