Гамильтоновы графы. Гамильтонов путь. Алгоритм нахождения гамильтоновых циклов в графе.
В 1859 г. известный математик сэр Уильям Гамильтон придумал игру, в которой требуется обойти замкнутый контур всех ребер додекаэдра, минуя каждую вершину лишь один раз.
Гамильтонов путь в графе G - это путь, содержащий все вершины графа G.
Гамильтоновым циклом в графе G называется цикл, содержащий все вершины графа G.
Граф G называется гамильтоновым, если он имеет гамильтонов цикл.
а) б)
а) Гамильтонов путь в графе.
б) Граф, в котором не существует гамильтонова пути.
а) б)
а) Гамильтонов граф.
б) Негамильтонов граф, имеющий гамильтонов путь.
Рассмотрим задачу нахождения гамильтонова пути, т.е. пути, проходящего только один раз через каждую вершину, а не каждое ребро графа. В отличие от эйлеровых путей не известно ни одного простого необходимого и достаточного условия для существования гамильтоновых путей и это несмотря на то, что эта задача - одна из центральных в теории графов. Не известен также алгоритм, который проверял бы существование гамильтонова пути в произвольном графе, используя число шагов, ограниченное многочленом от переменной n (числа вершин в графе). Проблема существования гамильтонова пути принадлежит к классу так называемых NP - полных задач.
Очевидный алгоритм, который мы можем применить, это "полный перебор всех возможностей". Генерируем все n! различных последовательностей вершин и для каждой из них проверяем, определяет ли она гамильтонов путь. Такие действия требуют по меньшей мере n!n шагов, но функция подобного вида растет быстрее, чем произвольная экспоненциальная функция an.
Опишем общий метод, позволяющий значительно сократить число шагов в алгоритмах типа полного перебора всех возможностей.
Алгоритмы с возвратом
Общим методом организации исчерпывающего поиска и позволяющим значительно сократить число шагов в алгоритмах типа полного перебора всех возможностей является так называемый возвратный ход по упорядоченному множеству частичных возможных решений. Этот метод - метод поиска с возвращением - основан на том, что мы многократно пытаемся расширить текущее частичное решение, или, что то же самое сузить множество тех возможных полных решений, которые не противоречат текущему частичному решению. Если расширение невозможно на текущем шаге поиска, то происходит возврат к предыдущему более короткому частичному решению и делается попытка его расширения, но уже другим способом.
Чтобы применить этот метод, искомое решение должно иметь вид последовательности <x1, x2, ..., xn>. В качестве начального частичного решения берется пустая последовательность ( ) (длины 0). Имея данное частичное решение <x1, x2, ..., xi>, мы стараемся найти такое допустимое значение xi+1, относительно которого мы не можем сразу заключить, что <x1, x2, ..., xi, xi+1> можно расширить до некоторого решения, либо <x1, x2, ..., xi, xi+1> уже является решением. Если такое предполагаемое, но еще не использованное значение xi+1 существует, то мы добавляем его к нашему частичному решению и продолжаем процесс для последовательности <x1, x2, ..., xi, xi+1>. Если его не существует, то мы возвращаемся к нашему частичному решению <x1, x2, ..., xi, xi-1> и продолжаем наш процесс, отыскивая новое, еще не использованное допустимое значение xi' - отсюда название "алгоритм с возвратом" (backtracking).
Мы предполагаем, что для каждого k > 0 существует некоторое множество Ak, из которого мы будем выбирать кандидатов для k-ой координаты частичного решения. В общем случае ограничения, описывающие решения, говорят о том из какого подмножества Sk множества Ak выбираются кандидаты для расширения частичного решения от <x1, x2, ..., xk-1> до <x1, x2, ..., xk>.
Если частичное решение <x1, x2, ..., xk-1> не предоставляет других возможностей для выбора нового xk, т.е. у частичного решения <x1, x2, ..., xk-1> либо нет кандидатов для расширения, либо все кандидаты к данному моменту уже использованы, то происходит возврат и осуществляется выбор нового элемента xk-1 из подмножества Sk-1. Если новый элемент xk-1 выбрать нельзя, т.е. к данному моменту множество Sk-1 уже пусто, то происходит еще один возврат и делается попытка выбрать новый элемент xk-2 и т.д.
Мы предполагаем, что существует некоторая простая функция, которая произвольному частичному решению <x1, x2, ..., xi> ставит в соответствие значение P(x1, x2, ..., xi) (истина либо ложь) таким образом, что если P(x1, x2, ..., xi) = ложь, то последовательность <x1, x2, ..., xi> нельзя расширить до решения. Если P(x1, x2, ..., xi) = истина, то мы говорим, что значение xi допустимо (для частичного решения <x1, x2, ..., xi-1>), но это отнюдь не означает, что <x1, x2, ..., xi-1> обязательно расширяется до полного решения.
Общую схему алгоритма, осуществляющего поиск с возвратом для нахождения всех решений, можно представить в следующем виде:
begin
k:=1;
while k>0 do
if существует еще неиспользованный элемент y Ak,
такой что P(x[1], ..., x[k-1], y) then
begin x[k]:=y; /* элемент y использован и удален из Sk */
if < x[1], ..., x[k]> является целочисленным решением
then
write (x[1], ..., x[k]); /* записать это решение */
k:=k + 1
end
else
k:=k - 1 /* возврат на более короткое частичное решение, все элементы множества Ak вновь становятся неиспользованными */
end.
/* все решения найдены */
Этот алгоритм находит все решения в предположении, что множества Ak конечные и все решения имеют длину меньше n.
Более коротко общую процедуру поиска с возвращением можно записать в рекурсивной форме.
Procedure AP (k);
/* генерирование всех решений, являющихся расширением последовательности x[1], ..., x[k-1]; массив x - глобальный */
begin
for y Ak такого, что P(x[1], ..., x[k-1], y) является решением do
begin x[k]:=y; /* элемент y использован и удален из Sk */
if < x[1], ..., x[k]> является целочисленным решением
then
write (x[1], ..., x[k]); /* записать это решение */
AP (k + 1)
end
end.
Генерирование всех целочисленных решений можно произвести вызовом AP (1), причем все возвраты скрыты в механизме, регулирующем рекурсию, т.е. в рекурсивном варианте "возврат" не появляются в явном виде, будучи частью реализации механизма рекурсии. Поэтому изучение алгоритма с возвратом мы начали с несколько более сложного нерекурсивного варианта.
Применим теперь алгоритм с возвратом для нахождения гамильтонова цикла в графе G=<V, E>. Каждый такой цикл можно представить последовательностью <x1, x2, ..., xn+1>, причем x1 = xn+1 = V0, где V0 - некоторая фиксированная вершина графа, {xi, xi+1} для 1 i n и xi xj для 1 i < j n..
Согласно этим условиям можно задать:
Ak = V - множество вершин,
P( x1, ..., xk-1, y) y rec [xk-1] y { x1, ..., xk-1}.
- логическое "и".
- "тогда и только тогда".