Зарегистрируйтесь для доступа к 15+ бесплатным курсам по программированию с тренажером

Алгоритм Литтла: Механизм работы Алгоритмы на графах

В прошлом уроке мы познакомились с задачей коммивояжера и решили ее с помощью перебора.

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

Он применяется к широкому классу задач на графах и для решения конкретной задачи его нужно адаптировать.

Алгоритм Литтла — известная адаптаций метода ветвей и границ для решения задачи коммивояжера. Его разработала группа ученых и программистов под руководством профессора Джона Д. К. Литтла. Статья с описанием алгоритма была опубликована в 1963 году.

Как работает алгоритм Литтла

Алгоритм Литтла довольно громоздкий, так что мы будем знакомиться с ним по частям. Для представления графа в алгоритме используется матрица смежности. В качестве иллюстрации мы будем использовать матрицу из оригинальной публикации 1963 года:

eyJpZCI6ImNiZjYwYjdkMmI3MmJhNzU5MDZhMDc0NzAyMTMyY2U3LnBuZyIsInN0b3JhZ2UiOiJjYWNoZSJ9?signature=bf1a4a0c2a13459efe4c0822a961d6ee8b5224d404267689dd1349a3622a3074

У нас есть шесть городов, поэтому в матрице шесть строк и шесть столбцов. Обсудим эту матрицу подробнее:

  • Числа в матрице — это стоимость переезда из одного города в другой. Это условная цифра, которая может обозначать цену бензина, расстояние между города или время езды

  • Обратите внимание, что граф ориентированный. Это значит, что стоимости перемещения и из необязательно равны между собой. В неориентированном графе эти стоимости совпадают

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

  • в первой строке в четвертом столбце — стоимость перемещения

  • в четвертой строке в первом столбце — стоимость перемещения

Некоторые ребра в графе не могут существовать физически — например, нельзя переместиться в тот же самый город. В матрице смежности таким переездам соответствуют ячейки:

  • В первой строке первом столбце

  • Во второй строке втором столбце

  • И так далее

Чтобы отличить невозможные переезды от возможных, мы придумаем для них особое обозначение.

В коде на JavaScript можно использовать константу Infinity — она соответствует бесконечности, которая больше любого конечного значения.

На иллюстрациях мы будем писать знак — символ бесконечности.

В матрице смежности маршрут записывается как последовательность переездов :

Длина этого маршрута равна:

Каждый город встречается в этом списке ровно два раза:

  • В строке — точка отправления

  • В столбце — точка прибытия

Это не совпадение — по условиям задачи, коммивояжер должен посетить каждый город ровно один раз. Мы можем отметить ребра маршрута на матрице смежности и увидеть, что в каждой строке и каждом столбце находится ровно одно ребро:

eyJpZCI6IjM1MWIyN2RkMDFkZGEyNDA5ZTBkMzAyYzk1MjZmMTIxLnBuZyIsInN0b3JhZ2UiOiJjYWNoZSJ9?signature=ba63016210e95b05a93e2a53f63a6a8ca2d69e3e1a9784194203d91fd7d53451

Это утверждение верно для всех маршрутов, удовлетворяющих условию задачи коммивояжера. Из этого следует два интересных факта:

  • Если из всех элементов строки или столбца вычесть одно и то же число, то все маршруты сократятся на это число

  • После такой модификации самый короткий маршрут останется самым коротким, а самый длинный — самым длинным

Алгоритм Литтла использует эти свойства для вычисления нижней границы.

Нижняя граница

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

Возьмем число . Если мы вычтем его из всех чисел строки или всех чисел столбца, то все маршруты сократятся на .

Если при этом все элементы в матрице останутся неотрицательными, то и новая длина маршрута будет больше нуля:

  • или

Возьмем нашу матрицу и найдем минимальное число в каждой строке. Запишем его справа от строки:

eyJpZCI6IjVmYjE3NjAwZDFjODY3MDMyOTkyMWVhZGYxYWIzYzA3LnBuZyIsInN0b3JhZ2UiOiJjYWNoZSJ9?signature=1d6e47e83a68d662ba8927b6a9ab9cc9f228b39ae4751b97ccf71a602ea34c6f

Вычтем из каждой строки минимальное число. Обратите внимание, что после такого вычитания все ячейки в матрице останутся неотрицательными:

eyJpZCI6IjQyNjcwODQxZTEyYWQwODI1MGIxNjA1ZjIwYWJlMmMxLnBuZyIsInN0b3JhZ2UiOiJjYWNoZSJ9?signature=a0b405c4ce74c5857668e199e219ff88b2a9c91da25cb6cde7026139c66671d3

При этом минимальная длина будет больше суммы чисел, которые мы вычли:

Такая операция в алгоритме Литтла называется редукцией по строкам.

Как мы говорили выше, подобную редукцию можно сделать и по столбцам. Найдем минимальное число в каждом столбце. Запишем его под столбцом:

eyJpZCI6IjUwYTE0MTM1Y2ExNjBlY2I5NzgyYzU5NDhjYTZmZDIzLnBuZyIsInN0b3JhZ2UiOiJjYWNoZSJ9?signature=4c48a3414aeed693de1cf3a7dcdcef25d4eda256e67416e50a1f3d579f84f4b9

Выполним редукцию столбцов — вычтем минимальное число из каждого столбца:

eyJpZCI6IjdiOGU0YTAxZjc2NjA1MDdlZTc4ZmU0YmVhNWExODcwLnBuZyIsInN0b3JhZ2UiOiJjYWNoZSJ9?signature=9643e38208eb9110c10e984dcf4ffc75c4796af758ae2307a6827ff314ff19fb

Все числа в матрице все еще неотрицательные, поэтому можно утверждать следующее:

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

Ветвление

На следующем шаге построим два поддерева. В начале работы алгоритма наш единственный узел — корень дерева, которое содержит все возможные маршруты:

300

Сейчас мы знаем два факта:

  • Нижняя граница длины маршрута в этом дереве равна

  • Пока у нас пока нет никакого маршрута, даже частичного

Предположим, в качестве очередного ребра маршрута мы выбрали . Построим два дочерних поддерева:

eyJpZCI6IjRiMGM3MWI2M2E3YjcwZWU0YTY2NDVjZjQ4MmRmODM4LnBuZyIsInN0b3JhZ2UiOiJjYWNoZSJ9?signature=a4bc16dafa23af35e330fd0cca33f4d0d44c3b4b60f1b8ff97dc125880ce9883

На половинах дерева мы видим два варианта:

  • Поддерево справа содержит маршруты, где есть ребро

  • Поддерево слева — маршруты, где его нет

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

Левое поддерево

Посмотрим, как изменится нижняя оценка для левого поддерева. Взглянем на редуцированную матрицу и выделим цветом ячейку, которая соответствует ребру :

eyJpZCI6IjRjMTgxZWQ1ZDMzMzdmZmY3NWQ4ZmQ5NmVjNWRlMDJkLnBuZyIsInN0b3JhZ2UiOiJjYWNoZSJ9?signature=31e68d9f7465e64d60e3596503dcbb36ab9c8617cbc7d0383f32beea9c0d9ba4

Рассмотрим каждую точку по отдельности. Сначала обсудим город :

  • Из него можно приехать в города и

  • Исключаем ребро из маршрута и оставим города и

  • Стоимость переезда в эти города равна и

  • Куда бы мы не переехали, стоимость не может быть меньше — это число мы видим в синей ячейке в первой строке

Перейдем к городу :

  • Из него можно приехать в города и

  • Исключаем ребро и оставляем города и

  • Стоимость переезда из этих городов равна и

  • Откуда бы мы не переехали, стоимость не может быть меньше — это число мы видим в синей ячейке в четвертом столбце

В итоге мы приходим к выводу — мы увеличим редуцированный маршрут минимум на в двух случаях:

  • Уехав из города в любой город, кроме

  • Приехав в город из любого города, кроме

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

Можно сказать, что — это штраф за отказ от ребра . Нам известна нижняя граница кратчайшего пути — . Без ребра она увеличится на и станет равна :

eyJpZCI6IjE1MTU3ODVhYzg0M2IyYzUwYjA3OGJkNjE4Mjc3MWJkLnBuZyIsInN0b3JhZ2UiOiJjYWNoZSJ9?signature=ea8a7d633a7a223af360d9c8bc9b4b76f1b81714234c262b32a7c1da340da02e

Предположим, на одном из этапов мы достроим одну из ветвей дерева до конца и получим вариант маршрута с длиной .

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

Чем выше нижняя граница в поддереве, тем больше шансов, что поддерево удастся отсечь. Именно поэтому полезно выбирать ребро с небольшим штрафом.

Штраф для элемента матрицы — это сумма минимумов в той же строке и том же столбце. Сам элемент учитывать не надо.

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

eyJpZCI6Ijg0ZGRmNWE0N2I2ZDA0N2Y2ZjQzYzJmODNlYjVlYzgzLnBuZyIsInN0b3JhZ2UiOiJjYWNoZSJ9?signature=09fb50abb7d280dcba2da0f3c0c6941c95bc59d9078a3a4762bb86337613751e

На рисунке цветом выделены все нулевые элементы. Рядом с каждым записан суммарный штраф. Максимальный штраф — он как раз и соответствует ребру :

Исключение ребра

Спускаясь по левому поддереву, мы должны помнить, что ребро в маршрут включать нельзя. Алгоритм Литтла предлагает хранить ее непосредственно в матрице смежности, поместив особое значение в первую строку четвертого столбца. Как и раньше, мы можем хранить очень большое число или константу Infinity.

Рассмотрим новую матрицу для левого поддерева. Обратите внимание, что она может оказаться нередуцированной, как получилось в нашем случае. Редуцируем ее по первой строке четвертого столбца:

eyJpZCI6ImQwNDg0NjFiOGVkYzNkZGRjMGU4MTNjMzU3MzY1MDc3LnBuZyIsInN0b3JhZ2UiOiJjYWNoZSJ9?signature=e3935d43094683bb3c3cb0d14cff1d07749029093d5ae4529af1cb33ff119dfd

Правое поддерево

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

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

Простой способ вычеркнуть строку и столбец — заполнить их значением .

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

На рисунке показана матрица после этих преобразований:

eyJpZCI6ImM4ZmE1NzZjM2JkYjE2NGU1OTlmZjQ2ODE2MTU1OTQxLnBuZyIsInN0b3JhZ2UiOiJjYWNoZSJ9?signature=dfb5189aed0414aa7f3e20da71b9ef6f74c846f7399e7a2d728aecbce73f7f5c

Ее можно редуцировать, вычтя минимумы из каждой строки и каждого столбца:

eyJpZCI6IjliNGViMTVmMDU3NzFiZWJkZTYzNDMyOTcwMDg4ZDIzLnBuZyIsInN0b3JhZ2UiOiJjYWNoZSJ9?signature=9827c1e08053834587191c3ef1a6c6f5829c0c34833a4065306de342a212077f

Посчитаем сумму минимумов:

Эту сумму минимумов надо прибавить к предыдущей нижней границе. Так мы получим нижнюю границу правого поддерева :

eyJpZCI6ImYwYjBhYWVhOThkNjgyYjc3ZGI5MTk1ZTZjNmFmYWM5LnBuZyIsInN0b3JhZ2UiOiJjYWNoZSJ9?signature=cd058ec1566a948bfab97e100b7629d3b0169cd9e2f2437627c117e9c712e507

Цикл

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

Берем матрицу правого поддерева. Она уже редуцирована, так что мы ищем нулевой элемент с максимальным штрафом:

eyJpZCI6ImUzMmQ3YjliYzcwMmVjNzdkZjI0YTEyZTIzMzUzOThjLnBuZyIsInN0b3JhZ2UiOiJjYWNoZSJ9?signature=46c8b6d967f9b4715ea792951ae725d0b08b204a68428d655a9ec65b04c3aadc

Максимальный штраф — , он соответствует ребру . Его мы и выберем для ветвления дерева. Как и на первом шаге, левое поддерево будет соответствовать маршрутам без ребра . В матрице левого поддерева достаточно записать во вторую строку первого столбца:

eyJpZCI6ImJlMjk0ODJhYjFlMTdhZDcyZDZjM2JlMjVmOGI2ZmZjLnBuZyIsInN0b3JhZ2UiOiJjYWNoZSJ9?signature=07d6b6454a4750c9811c5675c0882db0ceb830cf1fe8415222b2dcf3f02dc2a4

В правом поддереве мы вычеркиваем вторую строку первого столбца — заполняем их значением .

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

eyJpZCI6Ijc2YmE1MzE4M2Y5ZTI2NjBiZTM0ODA2NDFlMjg5MDNjLnBuZyIsInN0b3JhZ2UiOiJjYWNoZSJ9?signature=2acdb5a71570ba68eb32fa9fad5921aee02e3cbd663420f0c2321ae508c011f0

В алгоритме Литтла маршрут не строится последовательно, как это было в методе перебора. На каждом шаге мы выбираем ребро с максимальным штрафом. Два последовательно выбранных ребра могут даже не соединяться друг с другом.

В нашем примере ребра и соединены через вершину . Добавим к ним ребро и замкнем маршрут — правда, он будет пролегать не по всем шести городам, а только по трем.

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

Делаем редукцию получившейся матрицы:

eyJpZCI6Ijg1MjkwYmNhMTRmNTkwODhkODhlMmQxMDM5MzNlNGMyLnBuZyIsInN0b3JhZ2UiOiJjYWNoZSJ9?signature=534101c48018d3a41c4231a10fa86979ac2f417b8370dfac7cb958ff42be95e6

Нижняя граница для правого поддерева увеличивается на:

В итоге новая нижняя граница будет равна :

eyJpZCI6IjMyZTA5OWQ2MWJiZjAxOWQxMDM2OGRjMTM0ODkxYjc1LnBuZyIsInN0b3JhZ2UiOiJjYWNoZSJ9?signature=3319dc952c414f32736120a12eb5c2c75478bb171b936b9e59701bc0ba2e8ca3

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

Выбираем ребро с наибольшим штрафом :

eyJpZCI6IjUyOGEwOWYzYzk2YzYyODRlYTFiMDVhNzk3ZWQ5ODQ0LnBuZyIsInN0b3JhZ2UiOiJjYWNoZSJ9?signature=bb313a2095a44c78d84b3d528b4620b8198bddf035f10481c8d2082c7d2839f7

Продолжаем ветвление узла с наименьшей оценкой . Здесь максимальный штраф соответствует ребрам и . Для ветвления мы можем выбрать любое ребро, для примера пусть будет :

eyJpZCI6ImIyNGIxOGQxMDRkZDhmMGEyNTQwNTUyY2ZjM2VhNmU2LnBuZyIsInN0b3JhZ2UiOiJjYWNoZSJ9?signature=014f7291b43572d1e48f592198b4262dbb76595d9b74c2844ee4329766b2fe88

В самом правом поддереве можно образовать короткий маршрут из трех ребер:

Этот короткий цикл не является решением, поэтому вычеркиваем ребро из матрицы.

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

Возвращаемся к узлу, где вычеркнуто ребро . Разбиваем его на два поддерева:

eyJpZCI6IjE2ZWYyMDU2OTk5MDEwMjg5OWE5OTllYmFhZGM4M2M0LnBuZyIsInN0b3JhZ2UiOiJjYWNoZSJ9?signature=0b62f7c9518cd7813c89641ee67f683827306fd4f2e2dfb2851bf2a6db69b83a

Среди всех ребер есть с максимальным штрафом — . Таким образом:

  • Стоимость левого поддерева равна , если вычеркнуть ребро

  • Стоимость правого поддерева равна , если вычеркнуть ячейку и шестую строку третьего столбца

На схеме это выглядит так:

eyJpZCI6IjBkMTVjODZjOGJlOTI4NDg3ZGMxZTJiMDY5ODgxZDVjLnBuZyIsInN0b3JhZ2UiOiJjYWNoZSJ9?signature=e1175f5a6f4fe4882d100488db8a1317444d43e6c2f2055e6a08fd1b4c757b98

На этом этапе мы ни разу не проводили отсечение, потому что пока у нас нет ни одного построенного маршрута.

Сейчас в дереве решений есть два узла с минимальной нижней границей . Мы можем выбрать любое из них. Выберем самое правое поддерево и посмотрим на его матрицу:

eyJpZCI6ImRkYjJkM2E4YmVmNTY0NzQ1ZTFjYjAyMDJiZWYzZmY1LnBuZyIsInN0b3JhZ2UiOiJjYWNoZSJ9?signature=5e32e3553b8c971d5727230e035bc876708da16def51aa4c9342581b66548264

В этот момент мы останавливаемся, потому что у нас останется только два решения — два значения в матрице, которые меньше .

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

Нижняя граница длины маршрута в поддереве равна единственному маршруту в нем — а именно :

eyJpZCI6IjRiZGNjMzA5MzQ4MmU5MzJkYTVlZTdiN2NhZTAwYzMwLnBuZyIsInN0b3JhZ2UiOiJjYWNoZSJ9?signature=c4d0b842e1160dcd36c7d997cb87b8621cd4c72c3e4a4f1c28779da54f1ffe8b

Итак, мы построили первый маршрут длиной . У нас есть недостроенные поддеревья с нижними границами и . Нет ни одного поддерева с нижней границей меньше . Это значит, что в дереве решений точно нет маршрута, который мы уже нашли. Работу алгоритма можно завершить.

Если бы у нас оказалось несколько узлов с меньшей нижней границей, мы бы оставили только их, а остальные бы отсекли.

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

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

Выводы

В этом уроке мы познакомились с алгоритмом Литтла. Повторим ключевые выводы:

  • Алгоритм Литтла находит оптимальное решение в дереве решений

  • Для работы алгоритма важно, чтобы оценка нижней границы была корректной

  • Обычно алгоритм Литтла работает быстрее метода перебора, в худшем случае — с той же скоростью

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


Аватары экспертов Хекслета

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят команда поддержки Хекслета или другие студенты

Об обучении на Хекслете

Для полного доступа к курсу нужен базовый план

Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff

Используйте Хекслет по-максимуму!

  • Задавайте вопросы по уроку
  • Проверяйте знания в квизах
  • Проходите практику прямо в браузере
  • Отслеживайте свой прогресс

Зарегистрируйтесь или войдите в свой аккаунт

Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»
Изображение Тото

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