В следующих документах мы рассмотрим различные методы решения задачи коммивояжёра.
Сейчас же подготовим необходимую "базу" в виде класса Salesman.
Его конструктор создаёт массивы, необходимые для N городов:
functionSalesman(n)
{
this.N = n; // число городов
this.minWay = newArray(this.N); // лучший путь (массив номеров городов)
this.pos = newArray(this.N); // положение {x,y} городов на плоскости
this.way = newArray(this.N); // временный путь для различных алгоритмов
this.method = "perm"; // метод поиска пути ( см. init() )
this.timerPeriod = 10; // период (ms) вызова из таймера
this.timeTot = this.timeMin = 0; // время общее и до лучшего решения
}
Путь коммивояжёра, как и раньше, описываем
массивом неповторящихся целых чисел (номеров городов).
Путь зациклен и подразумевается, что из последнего
элемента массива (номера города) коммивояжёр переходит в нулевой элемент,
хранящий номер "начального" города.
Генератор расстояний
Для наглядности, заполним матрицу dists
евклидовыми расстояниями между точками на плоскости (плоская, евклидовая задача коммивояжёра).
Функция create создаёт города на карте размером w×h
в случайных местах. При этом используется генератор случайных чисел rnd
с затравочным значением seed
(будем использовать линейный конгруэнтный генераторlcg):
this.minWay[i] = this.way[i] = i; // пока лучший путь 0,1,2,...,N-1
this.minLen = this.distance(this.minWay); // пока лучшее расстояние
}
Если параметр round = true, то расстояние округляется до целых.
Длина любого пути way вычисляется в функции:
Salesman.prototype.distance = function(way)
{
vard = 0, i = 1;
for(; i < way.length; i++)
d += this.dists[way[i-1]][way[i]]; // из города way[i-1] в город way[i]
returnd + this.dists[way[i-1]][way[0]]; // замыкающее расстояние в город way[0]
}
Карта городов
Для рисования городов и пути коммивояжёра будем использовать класс draw,
который предоставляет методы рисования как на канвасе, так и в виде svg-картинки.
Следующая функция возвращает svg-картинку, которую можно непосредственно вставить в html-страницу:
Salesman.prototype.getSVG = function(way)
{
if(!way) way = this.minWay; // по умолчанию минимальный путь
vardraw = newDraw(), rect = {}; // объект рисования из draw.js
draw.transformAll(2*r, 2*r); // возвращаем рисунок как svg - текст:
returndraw.getSVG(rect.w+4*r, rect.h+4*r);
}
Иногда (особенно при использовании реальных городов) карта получается очень большая.
Для "вписывания" её в окне браузера, служит масштабный коэффициент scale.
В принципе, можно было не создавать нового массива координат pos,
а применить масштабное преобразования в самом конце. Однако линии при этом становятся слишком тонкими.
Поэтому, мы пересчитываем координаты в функции getMap и заполняем поля структуры rect = {w, h}
шириной и высотой результирующей карты:
Salesman.prototype.getMap = function(rect)
{
varmin = {x:Infinity, y:Infinity}, max = {x:0, y:0};
for(vari=0; i < this.pos.length; i++){ // минимальные и максимальные координаты
Обратим также внимание на свойство noNames в функции getSVG.
По умолчанию оно не определено. Если задать ему любое не нулевое значение, номера городов
выводиться не будут. Аналогично свойством noWay
(если оно задано) можно заблокировать рисование пути.
Задание радиуса города (в том числе нулевого) можно сделать "руками"
при помощи свойства radius.
Протестируем, что получилось, написав следующий скрипт (результат работы приведен справа):
varsm = newSalesman(8); // 8 городов
sm.create(200, 200, 4, lcg); // карта 120 x 120
document.write(sm.getSVG(),'<br>'); // как svg-картинку
Напомним, что класс Salesman должен быть описан внутри html-страницы (в любом месте).
Его можно также подключить из внешнего файла, написав
в заголовке страницы:
<script src="salesman.js"></script>
Естественно, заданный по умолчанию путь 0,1,2,... чаще всего оказывается не
оптимальным, что и получилось на рисунке. В данном случае кратчайший путь легко провести без
каких-либо вычислений (1,2,4,6,0,7,5,3) и его длина равна 528.29.
Длительные вычисления
Метод грубой силы и более "интеллектуальные" алгоритмы, при большом числе городов, занимают
длительное время.
Поэтому имеет смысл использовать таймер, при каждом вызове которого производится только часть вычислений.
Запускать и останавливать таймера будем нажатием кнопки, так как это было описано при обсуждении
"грубой силы".
При нажатии кнопки вызывается следующая функция:
Она (при первом нажатии) создаёт таймер (функция setInterval) и
передаёт ему функцию timer (см. ниже). Так как это не "обычная функция", а
метод класса, она передаётся при помощи дополнительного вызова bind (см. выше).
При повторном нажатии на кнопку будет вызвана функция stop,
которая уничтожит таймер:
this.btn.value = "run"; // меняем надпись на кнопке
}
Так как алгоритмов поиска пути несколько, введём свойство method класса Salesman,
которое необходимо задать перед запуском таймера. Сам таймер просто вызывает из себя
тот или иной метод вычислений:
Salesman.prototype.timer = function()
{
switch(this.method){
case"perm": this.permTimer(); break; // полный перебор
case"bound": this.boundInit(); break; // метод ветвей и границ
...
}
}
Перед запуском таймера вызывается функция init,
задающая общие и специфические для каждого метода начальные значения переменных:
Salesman.prototype.init = function()
{
this.cntWays = 0; // число просмотренных путей
this.cntNode = 0; // число сформированных узлов
this.cntCuts = 0; // число отсечений неподходящих веток
this.timeTot = this.timeMin = 0; // общее время и время до минимума
if(this.minWay.length !== this.N){ // на всякий случай, иначе не меняем
this.minWay = newArray(this.N); // лучший путь
this.way = newArray(this.N); // вспомогательный путь
this.outInfo(", countSol = "+ this.cntSol); // выводим результаты
}
Функция permNxt(ar, lf, rt) получения следующей перестановки в массиве ar в
диапазоне [lf ... rt] приведена в документе, посвящённом методу грубой силы.
Она, функция копирования copy и перестановки двух элементов массива местами swap
сделаны статическими. Кроме этого, определена функция вывода информации о процессе вычисления:
Свойство outTXT является объектом html-страницы в который
при помощи поля innerHTML можно выводить информацию о процессе вычислений.
Аналогично свойство outSVG служит для вывода svg-картинки с городами
и путём между ними.
Вставка в html-страницу
Чтобы увидеть результат работы полного перебора всех путей, вставим в html-страницу следующий текст: