В МИРЕ НАУКИScientific American · Издание на русском языке № 8 · АВГУСТ 1987 · С. 8487
Алгоритмические головоломки
А. К. ДЬЮДНИ
Города Задаченск и Решенск отстоят друг от друга на 100 км. Как-то погожим июньским днём в полдень из этих городов выходят навстречу друг другу два поезда, причём каждый движется со скоростью 50 км в час. В тот же момент пчела, уютно пристроившаяся на головной фаре поезда в Задаченске, вылетает в направлении Решенска и летит вдоль железнодорожного пути со скоростью 90 км в час. Встретившись с поездом, идущим из Решенска, пчела резко разворачивается и летит обратно всё с той же скоростью 90 км в час. Встретившись с поездом, идущим из Задаченска, она опять разворачивается и летит в противоположном направлении. Таким образом пчела снуёт между поездами до тех пор, пока они не встретятся. Какова будет полная длина пути, проделанного пчелой? Ответ к этой известной задачке, который будет дан в конце статьи, короткий. Это просто число.
Однако существуют задачи и другого рода, которые можно назвать алгоритмическими головоломками, и ответы к ним выглядят намного сложнее. Решить такую задачу значит указать последовательность процедур, позволяющих прийти к желаемому результату. Другими словами, ответ представляет собой алгоритм. В литературе, посвящённой головоломкам, приводится немало подобных задач. К числу известных относится, например, задача о том, как разделить жидкость в сосуде на три равные части посредством последовательности переливаний с использованием других сосудов определённой ёмкости или как можно без потерь с помощью одной лодки переправить на другой берег реки волка, козла и капусту. К этому же классу относится и задача о том, каким образом можно определить фальшивую монету (отличающуюся по массе) в кучке монет посредством последовательности взвешиваний на рычажных весах.
Различие в ответах на обычную задачу и алгоритмическую головоломку проще всего пояснить на конкретном примере. Начав с железнодорожных путей, пойдём по ним и дальше, продолжая тему первого примера. Допустим теперь, что поезда, движущиеся навстречу друг другу, при встрече не сталкиваются, а вовремя притормаживают. С шипением и грохотом они останавливаются друг перед другом. (При этом мне почему-то представляются два огромных старинных паровоза.) Между поездами лежит короткий отрезок железнодорожного полотна и маленькое боковое ответвление, длина которого позволяет разместить лишь один вагон или локомотив (см. рисунок).
Каким образом могут разминуться поезда?
Очевидно, кто-то сделал серьёзную ошибку при составлении расписания. Единственный способ выйти из затруднения и разминуться заключается в том, чтобы машинисты каким-то образом воспользовались боковым путём. В этой задаче так же, как и в последующих, мы будем предполагать, что каждый вагон и каждый локомотив с обеих сторон имеют устройство сцепления. Кроме того, на каждом поезде есть сцепщик, который, бегая по путям, может сцеплять и отцеплять вагоны.
Вытирая пот с лица красным в горошек носовым платком, машинист поезда, следующего из Задаченска, восклицает: «Я не знаю, как мы сможем пропустить друг друга. У нас есть только вон то небольшое ответвление». Машинист из Решенска настроен более оптимистично. Он излагает план, следуя которому поезда могут разминуться. Возможно ли это? Прежде чем читать дальше, давайте взглянем на рисунок. Для конкретности на нем изображены два поезда, каждый из пяти вагонов. В общей формулировке этой алгоритмической головоломки поезда имеют по n вагонов.
Подходя к решению этой сравнительно простой задачи, читатель, наверно, будет говорить себе под нос: «Так, посмотрим... А почему бы не провести состав из Задаченска по боковому пути по одному вагону? Поезд из Решенска мог бы сновать как челнок взад и вперёд, затаскивая каждый раз новый вагон задаченского поезда на боковой путь, затем, проезжая мимо него по основному пути на всю свою длину и прицепляя его с другой стороны, чтобы вывести с бокового пути перед тем, как вернуться за следующим вагоном».
Такое описание может послужить отправной точкой для построения ответа к алгоритмической головоломке, но его нужно сделать более ясным и чётким. Воспользуемся для этого алгоритмической системой записи, введя некоторые вспомогательные обозначения. При решении задачи можно выделить четыре участка железнодорожного полотна: участок от Задаченска до бокового пути (обозначим его A); отрезок основного пути между стрелками, соединяющими его с боковым путём (B); сам боковой путь (B') и участок от бокового пути до Решенска (C). Такая команда, как «Вперёд к A», означает, что поезд из Решенска, состоящий из локомотива и вагонов, прицепленных к нему на данный момент, продвигается вперёд до тех пор, пока полностью не окажется на участке пути A.
Напомним, что у каждого поезда по n вагонов. Локомотив задаченского поезда обозначим через P1, а его вагоны пронумеруем как P2, P3 и т.д. Команда «Прицепить Pk» означает, что к данному моменту решенский поезд находится вплотную к k-му элементу задаченского поезда; состав из Решенска мягко подкатывается к вагону Pk и сцепляется с ним. Те из вас, кто когда-нибудь жил вблизи железнодорожной станции, наверное, хорошо помнят очень характерный, ни на что другое не похожий звук, которым сопровождается сцепка вагонов.
Теперь можно записать алгоритм решения:
расцепить поезд P
for k=1 to n+1
вперёд к A
прицепить Pk
назад к C
вперёд к B'
отцепить Pk
назад к C
вперёд к A
назад к B'
прицепить Pk
назад к C
отцепить Pk
сцепить поезд P
Сначала поезд из Задаченска расцепляется на отдельные вагоны и мы имеем n вагонов и локомотив. Затем в алгоритме начинается цикл из 11 шагов. Всю работу выполняет поезд из Решенска. Ни разу не отцепив ни одного из своих вагонов, он сначала проходит вперёд на участок A, где сцепляется с первым элементом (k=1) задаченского состава, т.е. с локомотивом. Он буксирует локомотив назад к участку C и после перевода стрелок загоняет его на боковой путь B', здесь локомотив отцепляется. Затем состав из Решенска снова возвращается на участок C, стрелки переводятся, и он идёт вперёд к участку A. После этого он возвращается, проходит на боковой путь, снова прицепляется к оставленному там локомотиву задаченского состава и буксирует его на участок C, где отцепляет и оставляет его. Ту же последовательность шагов поезд выполняет для каждого из оставшихся вагонов задаченского состава. Когда главный цикл завершён, все вагоны задаченского поезда оказываются на участке дороги между решенским составом и Решенском. Теперь, как только поезд придёт в себя от утомительных алгоритмических процедур, он запыхтит и тронется в направлении к Решенску.
Ещё одна алгоритмическая головоломка возникает, когда поезд из Задаченска, завершая своё путешествие, подъезжает к Решенску. Машинист, глядя на пробегающие мимо него пригороды, вдруг с ужасом вспоминает, что забыл взять с собой пакетик с бутербродами. Делать нечего, придётся возвращаться обратно. Он нажимает на тормоза, и постепенно тяжёлый состав из n вагонов останавливается. Пятиться всю дорогу до Задаченска не очень-то удобно. К счастью, машинист замечает справа небольшое боковое ответвление тупик. Его длина достаточна лишь для одного вагона, у разветвления есть управляющие стрелки. Вдохновлённый алгоритмическим достижением машиниста из Решенска, наш скромный герой тоже хочет попробовать свои силы.
Прежде всего машинист задаченского состава рисует на клочке бумаги схему пути (см. рисунок).
Как поезд может развернуться, чтобымашинист вернулся в город, откуда прибыл?
Затем он расписывает алгоритм и тщательно проверяет его, водя пальцем по бумаге то туда, то сюда и бормоча что-то себе под нос. После проверки алгоритма он начинает длительную серию отцеплений, буксировок и сцеплений, пока это всё не заканчивается тем, что поезд оказывается полностью развёрнутым в обратном направлении. Причём не только порядок, в котором следовали локомотив и вагоны, сохранился, но и сами вагоны и локомотив оказались повёрнутыми в обратном направлении. Каким образом машинист проделал этот манёвр? Лучшее решение читателей будет опубликовано в одном из следующих номеров журнала. Под словом «лучшее» подразумевается наиболее ясное и наиболее остроумное решение. Эта задача не так бесхитростна, как может показаться на первый взгляд.
Уже на пути в Задаченск машинист понял, что решение задачи потребовало очень больших энергетических затрат. Угольный тендер паровоза оказался почти пустым. Количество работы, затраченной во время маневров поезда, было пропорционально n3. Только теперь, по пути домой, машинист задаченского поезда подумал о том, что, возможно, существует более эффективное решение. Давайте подумаем тоже, возможно ли развернуть состав за меньшее число шагов, скажем, порядка n2 или даже nlogn?
Решение
Конечно, очень трудно было отобрать лучшее решение из множества ответов, присланных к головоломке, в которой требовалось развернуть весь состав, пользуясь лишь одним тупиком, вмещавшим только один вагон. Отобрав более или менее произвольно одно из лучших решений, присланных раньше других, я представляю здесь алгоритм, предложенный Д. Ауенбаем из Уэст-Ковина (шт. Калифорния). Буквой A обозначен участок железнодорожного полотна от Задаченска до тупика, B обозначает сам тупик и C основной участок пути от тупика B до Решенска. Через Pk обозначен k-й вагон поезда.
Расцепить поезд P
вперёд к B
назад к C
for k=1 to n
вперёд к A
прицепить Pk
назад к C
вперёд к B
отцепить Pk
назад к C
вперёд к A
назад к B
прицепить Pk
В другом варианте решения процедура начинается не с первого вагона, а с последнего. Как подметили некоторые читатели, приведённое выше решение требует затрат, пропорциональных n3, поскольку n вагонов поезда нужно протащить назад и вперёд n раз на пути, длина которого приблизительно равна длине состава из n вагонов. М. Блам, специалист по вычислительной математике из Калифорнийского университета в Беркли, смог придумать такой алгоритм, который решает эту задачу при затратах работы, пропорциональных n2 logn. К сожалению, недостаток места не позволяет мне привести здесь алгоритм Блама.
Чтобы ещё лучше потренировать однорельсовые умы в искусстве алгоритмических головоломок, вот ещё одна маленькая задачка. Локомотив без вагонов приближается к поворотному кругу, на котором стоят два пустых вагона. Между вагонами расположен небольшой железнодорожный мост, прочность которого позволяет выдержать один вагон, но локомотива он уже не выдержит (см. рисунок).
Локомотив должен поменять вагоны местами,не пересекая опасного моста
Машинист локомотива должен развернуть вагоны так, чтобы они поменялись местами. Как и в предыдущих задачах, все сцепления и расцепления должны производиться только в неподвижном состоянии. Длина моста не превышает длины одного вагона. Завершив свою работу, локомотив должен покинуть поворотный круг. Решение задачи представляет собой простой алгоритм без циклов. С этим алгоритмом читатели ознакомятся в очередном номере журнала.
Решение
Локомотив заходит на круговой путь, подходит к вагону A и заталкивает его на мост. Затем он пятится назад, подходит к вагону B, прицепляет его, подводит к краю моста и сцепляет вагоны A и B. Далее локомотив и прицепленные к нему вагоны, пройдя через стрелку, выезжают на прямой путь, где вагон A отцепляется. Теперь локомотив опять подводит вагон B к мосту и, отцепив, оставляет его там. Наконец, локомотив проходит по кругу, стягивает вагон B с моста, оставляет его на новом месте и возвращается за вагоном A.
Со следующими двумя алгоритмическими головоломками я не был знаком раньше. Они продолжают транспортную тему, начатую в задачах о поездах, однако переводят наши исследования на более высокую ступень теперь мы будем заниматься грузовиками. Задачи этой серии я буду называть задачами разведчика пустыни. В первой задаче патрульный автомобиль разведчика пустыни имеет бак, вмещающий 10 галлонов бензина. Бак автомобиля заполняется из цистерн ёмкостью 50 галлонов. Несколько таких цистерн хранятся на складе. Если автомобиль может везти в качестве груза одну цистерну с бензином и если он затрачивает 1 галлон горючего на 10 миль пути независимо от того, вёзет он с собой цистерну или нет, то каково максимальное расстояние, на которое он может удалиться от базы, прежде чем у него иссякнет горючее? Ответ, конечно, зависит от количества цистерн с бензином, хранящихся на базе. Как и во всех алгоритмических головоломках, предположим, что число цистерн на базе равно n.
На какое максимальное расстояние сможет уехать грузовик,располагая n цистернами с горючим?
Если n=1, то ответ получить несложно. Автомобиль заправляет свой бак из единственной цистерны, грузит её в кузов и отправляется в путь под жарким солнцем пустыни. Заправляясь при необходимости из цистерны, автомобиль, очевидно, пройдёт 500 миль, прежде чем горючее будет полностью исчерпано. А как далеко он сможет уехать на двух цистернах? Поскольку автомобиль может везти с собой лишь одну цистерну, то, очевидно, он отвезёт первую цистерну на какое-то расстояние в пустыню, заправится из неё, если это необходимо, а затем вернётся на базу за второй. Вторую цистерну разведчик повезёт по той же дороге. Он может остановиться для заправки, а может и не останавливаться у первой, оставленной им в пустыне цистерны, прежде чем продолжить свое путешествие. Как далеко он сможет уехать, совершая такие челночные перемещения между пунктами, в которых он оставляет запасы горючего? Внезапно задача перестаёт казаться тривиальной.
Приведём короткий алгоритм, позволяющий патрульному автомобилю проехать путь длиной 600 миль:
заправить бак и погрузить первую цистерну;
вперёд 100 миль;
снять цистерну и заправиться;
назад на базу за второй цистерной;
заправиться и погрузить цистерну;
вперёд 100 миль;
заправиться из первой цистерны;
вперёд 100 миль;
снять цистерну и заправиться;
назад к первой цистерне;
заправиться и погрузить цистерну;
вперёд 100 миль.
На этой стадии алгоритма автомобиль удалился от базы на 200 миль. У него две цистерны: в одной осталось 10 галлонов, а в другой 30 галлонов бензина. Поездка продолжается после того, как водитель заправляет машину из первой цистерны (оставляя её пустой), погружает в кузов вторую цистерну и уезжает, скрываясь за барханами. Таким образом, он сможет проехать ещё 400 миль, прежде чем у него кончится горючее. Полное расстояние от базы составит 600 миль. Поскольку увеличение расстояния по сравнению с первым случаем, когда на базе была лишь одна цистерна, составляет всего 100 миль, у читателя, наверное, возникнет справедливое подозрение, что существует лучший алгоритм решения этой задачи. Действительно, можно добиться лучшего результата.
Ну а как далеко сможет уехать разведчик пустыни, если на базе имеется n цистерн с бензином? Как и во всех алгоритмических головоломках, простой численный ответ или даже формула не годятся. Требуется составить алгоритм, дающий тот или иной ответ.
Решение
Если n=2, то оптимальное расстояние, как показали У. Липп из Милфорда (шт. Коннектикут) и многие другие читатели, равно 733,33 мили. В своём живо написанном письме под заглавием «Побьём рекорд! Даёшь 733 мили всего на двух цистернах!» Липп показал, как можно превзойти результат в 600 миль. Вот его алгоритм (в слегка отредактированной форме):
заправить автомашину из первой цистерны;
погрузить вторую цистерну;
вперёд на 50 миль;
снять вторую цистерну;
назад на базу;
заправиться из первой цистерны;
погрузить первую цистерну;
вперёд на 100 миль;
снять первую цистерну;
заправиться из первой цистерны;
назад на 50 миль ко второй цистерне;
погрузить вторую цистерну;
вперёд на 50 миль к первой цистерне;
заправиться из первой цистерны;
вперёд на 33,33 мили;
снять вторую цистерну;
назад на 33,33 мили к первой цистерне;
погрузить первую цистерну;
вперёд на 33,33 мили ко второй цистерне.
На этом этапе патрульная автомашина находится уже на расстоянии 133,33 мили от базы. Теперь нужно просто погрузить вторую цистерну (ещё полную) и проехать вперёд на расстояние 600 миль.
Обобщая этот алгоритм, мы не сможем прийти к оптимальному алгоритму при числе цистерн n>2. Например, при его помощи мы не сможем достичь оптимального расстояния при наличии 3 цистерн с горючим. Алгоритмы, представленные Ч. Новогорски из Нэплза (шт. Флорида) и Н. Рокком из Уинтерсвиля (шт. Огайо), свидетельствуют о том, что при n=3 автомашина может удалиться от базы на расстояние 860 миль. Результаты большинства других читателей, попробовавших свои силы в этой задаче, были меньше этой величины. На самом деле даже общие формулы, представленные большинством читателей, дали более низкий результат, когда вместо n, количества цистерн в общем случае, было подставлено число 3. Я не могу поэтому поручиться за правильность формул, подобных той, которую прислал Л. Лейнвебер из Кливленда (шт. Огайо). Однако она является типичным представителем тех формул из числа представленных читателями, которые дают наилучшие результаты:
n
5
∑
100
2k1
100
2n1
+ 100.
k=1
В другой части громадной песчаной территории проходит патрульный маршрут противника, объезжаемый им другим способом. В произвольных пунктах замкнутого маршрута самолёт сбрасывает n цистерн с горючим. В каждой цистерне содержится определённое количество бензина, однако это количество может варьировать в значительных пределах от одной цистерны к другой (см. рисунок ниже). Поскольку этот маршрут пролегает через неприятельскую территорию, машина и водитель сбрасываются на парашютах. Они приземляются у одной из цистерн, заправляются и начинают объезд. Бензобак машины растягивающийся: он может вместить любое количество горючего независимо от того, какой ёмкости цистерна ему встретится на пути.
Откуда должна начать путешествие машина,чтобы успешно объехать весь маршрут?
Как ни странно, по мере того как машина продвигается по маршруту, горючее никогда не оказывается исчерпанным, прежде чем она достигнет следующего пункта заправки. Другими словами, полное количество горючего в цистернах таково, что его как раз достаточно, чтобы машина могла полностью объехать маршрут, не больше и не меньше. Это странно, потому что, как я уже говорил, выбор пунктов для расположения цистерн с горючим и его количество в каждой цистерне совершенно произвольны. Например, вполне может случиться так, что машина начнёт путешествие от цистерны, не содержащей достаточного количества горючего для того, чтобы можно было добраться до другой ближайшей цистерны, и разведчик застрянет посреди пустыни. В чём тут секрет?
Наш разведчик пустыни знает об этих патрульных поездках противника и откровенно озадачен ими. О противнике известно, что он сумасшедший и необыкновенно удачливый. Младший по чину помощник нашего разведчика, который проводит бессонные ночи в своей палатке, решая различные головоломки, находит ответ. «По-видимому, сэр, дело в том, что независимо от того, куда сбрасываются цистерны и сколько горючего они содержат, всегда можно найти такой пункт, начав с которого противник сумеет пройти весь маршрут».
Предлагаю читателям самим поразмыслить и открыть секрет. Разумеется, речь идёт об алгоритме. Требуется шаг за шагом описать процедуру, в результате выполнения которой можно найти по крайней мере один заправочный пункт, где машина может начать (и завершить) свою патрульную поездку. Алгоритм должен также указывать направление, в котором следует двигаться машине. Ошибка в направлении может привести к катастрофе. Лучшие ответы к этой задаче будут опубликованы в одном из ближайших номеров журнала.
Решение
Ряд читателей, включая А. Лавриджа из Лонг-Бича (шт. Калифорния), придумали очень интересный приём, при помощи которого можно решить эту задачу. Представим себе, что автомашина совершает «пробную» поездку, стартуя у произвольно выбранного склада и двигаясь в произвольном направлении. Попутно будем рисовать график количества бензина в баке, как функцию пройденного расстояния, и не будем останавливаться, даже если горючее полностью исчерпалось. В этом случае график просто перейдёт в отрицательную область. После каждой заправки на складе горючего кривая резко поднимается вверх и затем начинает постепенно снижаться. В конце концов машина вернётся к исходной точке. Теперь водитель должен проанализировать график и выбрать тот склад, при приближении к которому машина имела минимальное количество бензина (перед заправкой). От этого склада и следует начинать поездку.
В начале статьи я обещал дать ответ на задачу о двух поездах и пчеле. Интересные головоломки часто отличаются тем, что вводят в заблуждение решающего, в особенности когда задача имеет простое решение. Описывая бесконечный процесс, в котором пчела мечется между двумя поездами, я намеренно пытался запутать читателей. Чтобы решить задачу, необходимо лишь понять, что пчела находится в полёте ровно столько времени, сколько требуется поездам для того, чтобы встретиться. Половину расстояния между Задаченском и Решенском они преодолевают ровно за час. Следовательно, пчела, скорость полёта которой всегда равна 90 км/ч, пролетит 90 км.
С этой задачей связана одна легенда, за правдивость которой я, впрочем, не могу поручиться. Говорят, что в своё время кто-то задал эту задачку Джону фон Нейману, одному из величайших математиков XX столетия. Он тотчас ответил:
Конечно, 90 километров.
Я так и знал, сказал спрашивающий его, что вы найдёте лёгкий способ.
Какой лёгкий способ? сказал фон Нейман.
Оказывается, он просуммировал в уме бесконечный ряд. [Ещё один пример вычислительных способностей фон Неймана. E.G.A.]