Задача коммивояжера, ДП по подмножествам — различия между версиями

Материал из Викиконспекты
Перейти к: навигация, поиск
(Реализация)
Строка 1: Строка 1:
'''Задача о коммивояжере''' (англ. '''Travelling - salesman problem, TSP''') - это задача, в которой определяется кратчайший замкнутый путь, соединяющий заданное множество, которое состоит из <tex> N </tex> точек на плоскости.
+
'''Задача о коммивояжере''' (англ. '''Travelling - salesman problem, TSP''') - это задача, в которой определяется кратчайший замкнутый путь, соединяющий заданное множество, которое состоит из <tex> N </tex> точек на плоскости. Коммивояжер должен посетить <tex> N </tex> городов, побывав в каждом из них ровно по одному разу и завершив путешествие в том городе, с которого он начал. В какой последовательности ему нужно обходить города, чтобы общая длина его пути была наименьшей?
 
 
== Формулировка задачи ==
 
Коммивояжер должен посетить <tex> N </tex> городов, побывав в каждом из них ровно по одному разу и завершив путешествие в том городе, с которого он начал. В какой последовательности ему нужно обходить города, чтобы общая длина его пути была наименьшей?
 
  
 
== Варианты решения  ==
 
== Варианты решения  ==
Строка 8: Строка 5:
  
 
Так вот задача о коммивояжере относится к классу NP-полных задач. Рассмотрим два варианта решения.
 
Так вот задача о коммивояжере относится к классу NP-полных задач. Рассмотрим два варианта решения.
 +
 
==== Перебор перестановок ====
 
==== Перебор перестановок ====
 
+
Можно решить задачу перебором всевозможных перестановок. Для этого нужно сгенерировать все <tex> N! </tex> всевозможных перестановок вершин исходного графа, подсчитать для каждой перестановки длину маршрута и выбрать минимальный из них. Но тогда задача оказывается неосуществимой даже для достаточно небольших <tex>N</tex>. Сложность алгоритма  <tex>O({N!}\times{N})</tex>.
Можно решить задачу перебором всевозможных перестановок. Для этого нужно сгенерировать все <tex> N! </tex> всевозможных перестановок вершин исходного графа, подсчитать для каждой перестановки длину маршрута и выбрать минимальный из них. Но тогда задача оказывается неосуществимой даже для достаточно небольших <tex>N</tex>. Сложность алгоритма  <tex>O(N!)</tex>.
 
  
 
==== Динамическое программирование по подмножествам (по маскам) ====
 
==== Динамическое программирование по подмножествам (по маскам) ====
Строка 16: Строка 13:
 
Задача о коммивояжере представляет собой поиск кратчайшего гамильтонова цикла в графе.
 
Задача о коммивояжере представляет собой поиск кратчайшего гамильтонова цикла в графе.
  
Смоделируем данную задачу при помощи графа. При этом вершинам будут соответствовать города, а ребрам - дороги. Пусть в графе <tex> G =(V, E)</tex>  <tex> N </tex>
+
Смоделируем данную задачу при помощи графа. При этом вершинам будут соответствовать города, а ребрам - дороги. Пусть в графе <tex> G?=?(V,?E)</tex>  <tex> N </tex>
вершин, пронумерованных от <tex>0</tex> до <tex>N-1</tex> и каждое ребро <tex>(i, j) \in E </tex> имеет некоторый вес <tex> p(i, j)</tex>. Необходимо найти гамильтонов цикл, сумма весов по ребрам которого минимальна.
+
вершин, пронумерованных от <tex>0</tex> до <tex>N-1</tex> и каждое ребро <tex>(i, j) \in E </tex> имеет некоторый вес <tex> w(i,j)</tex>. Необходимо найти гамильтонов цикл, сумма весов по ребрам которого минимальна.
  
Зафиксируем начальную вершину <tex>s</tex> и будем искать гамильтонов цикл наименьшей стоимости - путь от <tex>s</tex> до <tex>s</tex>, проходящий по всем вершинам(кроме первоначальной) один раз. Т.к. искомый цикл проходит через каждую вершину, то выбор <tex>s</tex> не имеет значения. Поэтому будем считать <tex>S = 0 </tex>.
+
Зафиксируем начальную вершину <tex>s</tex> и будем искать гамильтонов цикл наименьшей стоимости - путь от <tex>s</tex> до <tex>s</tex>, проходящий по всем вершинам(кроме первоначальной) один раз. Т.к. искомый цикл проходит через каждую вершину, то выбор <tex>s</tex> не имеет значения. Поэтому будем считать <tex>s = 0 </tex>.
  
 
Подмножества вершин будем кодировать битовыми векторами, обозначим <tex>mask_i</tex> значение <tex>i</tex>-ого бита в векторе <tex>mask</tex>.
 
Подмножества вершин будем кодировать битовыми векторами, обозначим <tex>mask_i</tex> значение <tex>i</tex>-ого бита в векторе <tex>mask</tex>.
Строка 32: Строка 29:
 
\begin{cases}
 
\begin{cases}
 
  0, &\text{if }i = 0\text{ and }mask = 0 \\
 
  0, &\text{if }i = 0\text{ and }mask = 0 \\
  min_{j: mask_j=1, (i, j) \in E} \begin{Bmatrix} p(i, j) + d[j][mask - 2^j] \end{Bmatrix}, & \text{if } i\neq 0 \text{ or } mask \neq 0\\
+
  min_{j: mask_j=1, (i, j) \in E} \begin{Bmatrix} w(i, j) + d[j][mask - 2^j] \end{Bmatrix}, & \text{if } i\neq 0 \text{ or } mask \neq 0\\
 
  \infty, & \text{if } i \neq 0 \text{ and } mask \neq 0 \text{ and set of possible transitions is empty}
 
  \infty, & \text{if } i \neq 0 \text{ and } mask \neq 0 \text{ and set of possible transitions is empty}
 
\end{cases}
 
\end{cases}
Строка 40: Строка 37:
  
  
Стоимостью минимального гамильтонова цикла в исходном графе будет значение <tex> d[0][2^n-1]</tex> - стоимость пути из <tex>0</tex>-й вершины в <tex>0</tex>-ю, при необходимости посетить все вершины.
+
Стоимостью минимального гамильтонова цикла в исходном графе будет значение <tex> d[0][2^n-1]</tex> - стоимость пути из <tex>0</tex>-й вершины в <tex>0</tex>-ю, при необходимости посетить все вершины. Данное решение требует <tex>O({2^n}\times{n})</tex> памяти и <tex>O({2^n}\times{n^2})</tex> времени.
 
 
Восстановить сам цикл несложно. Для этого воспользуемся соотношением <tex> d[i][mask] = p(i, j) + d[j][mask - 2^j] </tex>,  которое выполняется для всех ребер, входящих в минимальный цикл . Начнем с состояния <tex> i = 0 </tex>, <tex> mask = 2^n - 1</tex>, найдем вершину <tex>j</tex>, для которой выполняется указанное соотношение, добавим <tex>j</tex> в ответ, пересчитаем текущее состояние как <tex>i = j</tex>, <tex> mask = mask - 2^j </tex>. Процесс заканчивается в состоянии <tex>i = 0</tex>, <tex> mask = 0 </tex>.
 
  
Данное решение требует <tex>O({2^n}\times{n})</tex> памяти и <tex>O({2^n}\times{n^2})</tex> времени.
+
Для того, чтобы восстановить сам пут, воспользуемся соотношением <tex> d[i][mask] = w(i, j) + d[j][mask - 2^j] </tex>,  которое выполняется для всех ребер, входящих в минимальный цикл . Начнем с состояния <tex> i = 0 </tex>, <tex> mask = 2^n - 1</tex>, найдем вершину <tex>j</tex>, для которой выполняется указанное соотношение, добавим <tex>j</tex> в ответ, пересчитаем текущее состояние как <tex>i = j</tex>, <tex> mask = mask - 2^j </tex>. Процесс заканчивается в состоянии <tex>i = 0</tex>, <tex> mask = 0 </tex>.
  
 
== Реализация ==
 
== Реализация ==
 
   //Все переменные используются из описания алгоритма, inf = бесконечность
 
   //Все переменные используются из описания алгоритма, inf = бесконечность
  inputData(); //считывание данных
 
 
   d[0][0] = 0;
 
   d[0][0] = 0;
 
   for i = 0 to n - 1
 
   for i = 0 to n - 1
     for mask = 0 to mask = 2 ^ n - 1
+
     for mask = 0 to mask = 2 ** n - 1
 
       for j = 0 to n - 1
 
       for j = 0 to n - 1
 
         if j-ий бит mask == 1
 
         if j-ий бит mask == 1
           if p(i, j) существует
+
           if w(i, j) существует
             d[i][mask] = min(d[i][mask], d[j][mask - 2 ^ j]);
+
             d[i][mask] = min(d[i][mask], d[j][mask - 2 ** n] + w(i, j);
 
           else
 
           else
 
             d[i][mask] = inf;
 
             d[i][mask] = inf;
   writeData(); // запись данных, ответ хранится в d[0][2 ^ n - 1]
+
   print d[0][2 ** n - 1];
  
 
== Ссылки ==  
 
== Ссылки ==  

Версия 17:41, 1 декабря 2011

Задача о коммивояжере (англ. Travelling - salesman problem, TSP) - это задача, в которой определяется кратчайший замкнутый путь, соединяющий заданное множество, которое состоит из [math] N [/math] точек на плоскости. Коммивояжер должен посетить [math] N [/math] городов, побывав в каждом из них ровно по одному разу и завершив путешествие в том городе, с которого он начал. В какой последовательности ему нужно обходить города, чтобы общая длина его пути была наименьшей?

Варианты решения

В теории алгоритмов NP-полная (NPC, NP-complete) задача — задача из класса NP, к которой можно свести любую другую задачу из класса NP за полиномиальное время. Таким образом, NP-полные задачи образуют в некотором смысле подмножество «самых сложных» задач в классе NP; и если для какой-то из них будет найден «быстрый» алгоритм решения, то и любая другая задача из класса NP может быть решена так же «быстро». Cтатус NP-полных задач пока что неизвестен. Для их решения до настоящего времени не разработано алгоритмов с полиномиальным временем работы, но и не доказано, что для какой-то из них алгоритмов не существует. Этот так называемый вопрос P[math]\neq[/math]NP с момента своей постановки в 1971 году стал одним из самых трудных в теории вычислительных систем.

Так вот задача о коммивояжере относится к классу NP-полных задач. Рассмотрим два варианта решения.

Перебор перестановок

Можно решить задачу перебором всевозможных перестановок. Для этого нужно сгенерировать все [math] N! [/math] всевозможных перестановок вершин исходного графа, подсчитать для каждой перестановки длину маршрута и выбрать минимальный из них. Но тогда задача оказывается неосуществимой даже для достаточно небольших [math]N[/math]. Сложность алгоритма [math]O({N!}\times{N})[/math].

Динамическое программирование по подмножествам (по маскам)

Задача о коммивояжере представляет собой поиск кратчайшего гамильтонова цикла в графе.

Смоделируем данную задачу при помощи графа. При этом вершинам будут соответствовать города, а ребрам - дороги. Пусть в графе [math] G?=?(V,?E)[/math] [math] N [/math] вершин, пронумерованных от [math]0[/math] до [math]N-1[/math] и каждое ребро [math](i, j) \in E [/math] имеет некоторый вес [math] w(i,j)[/math]. Необходимо найти гамильтонов цикл, сумма весов по ребрам которого минимальна.

Зафиксируем начальную вершину [math]s[/math] и будем искать гамильтонов цикл наименьшей стоимости - путь от [math]s[/math] до [math]s[/math], проходящий по всем вершинам(кроме первоначальной) один раз. Т.к. искомый цикл проходит через каждую вершину, то выбор [math]s[/math] не имеет значения. Поэтому будем считать [math]s = 0 [/math].

Подмножества вершин будем кодировать битовыми векторами, обозначим [math]mask_i[/math] значение [math]i[/math]-ого бита в векторе [math]mask[/math].

Обозначим [math]d[i][mask][/math] как наименьшую стоимость пути из вершины [math]i[/math] в вершину [math]0[/math], проходящую (не считая вершины [math]i[/math]) единожды по всем тем и только тем вершинам [math]j[/math], для которых [math]mask_j = 1[/math] (т.е. [math]mask[/math] - подмножество вершин исходного графа, которые осталось посетить).

Конечное состояние - когда находимся в 0-й вершине, все вершины посещены (т.е. [math]i = 0[/math], [math]mask = 0[/math]). Для остальных состояний перебираем все возможные переходы из [math]i[/math]-й вершины в одну из непосещенных ранее и выбираем способ, дающий минимальный результат. Если возможные переходы отсутствуют, решения для данной подзадачи не существует (обозначим ответ для такой подзадачи как [math]\infty[/math]).

То есть, [math]d[i][mask][/math] считается по следующему правилу:

[math] d[i][mask] = \begin{cases} 0, &\text{if }i = 0\text{ and }mask = 0 \\ min_{j: mask_j=1, (i, j) \in E} \begin{Bmatrix} w(i, j) + d[j][mask - 2^j] \end{Bmatrix}, & \text{if } i\neq 0 \text{ or } mask \neq 0\\ \infty, & \text{if } i \neq 0 \text{ and } mask \neq 0 \text{ and set of possible transitions is empty} \end{cases} [/math]

где, [math] \text {and ---}[/math] и, [math]\text {or ---} [/math] или, [math]\text {set of possible transitions is empty ---}[/math] множество возможных переходов пусто.


Стоимостью минимального гамильтонова цикла в исходном графе будет значение [math] d[0][2^n-1][/math] - стоимость пути из [math]0[/math]-й вершины в [math]0[/math]-ю, при необходимости посетить все вершины. Данное решение требует [math]O({2^n}\times{n})[/math] памяти и [math]O({2^n}\times{n^2})[/math] времени.

Для того, чтобы восстановить сам пут, воспользуемся соотношением [math] d[i][mask] = w(i, j) + d[j][mask - 2^j] [/math], которое выполняется для всех ребер, входящих в минимальный цикл . Начнем с состояния [math] i = 0 [/math], [math] mask = 2^n - 1[/math], найдем вершину [math]j[/math], для которой выполняется указанное соотношение, добавим [math]j[/math] в ответ, пересчитаем текущее состояние как [math]i = j[/math], [math] mask = mask - 2^j [/math]. Процесс заканчивается в состоянии [math]i = 0[/math], [math] mask = 0 [/math].

Реализация

 //Все переменные используются из описания алгоритма, inf = бесконечность
 d[0][0] = 0;
 for i = 0 to n - 1
   for mask = 0 to mask = 2 ** n - 1
     for j = 0 to n - 1
       if j-ий бит mask == 1
         if w(i, j) существует
           d[i][mask] = min(d[i][mask], d[j][mask - 2 ** n] + w(i, j);
         else
           d[i][mask] = inf;
 print d[0][2 ** n - 1];

Ссылки

Литература

  • Романовский И. В. Дискретный анализ. СПб.: Невский Диалект; БХВ-Петербург, 2003. ISBN 5-7940-0114-3
  • Кормен Т., Лейзерсон Ч., Ривест Р., Штайн К. Алгоритмы: построение и анализ, 2-е издание. М.: Издательский дом "Вильямс", 2005. ISBN 5-8459-0857-4