Метод грубой силы
Задача коммивояжёра
Dij ≥ 0, Dij = Dji.
Вообще говоря, Dij - не обязательно "физическая длина" дороги. Это может быть время перемещения, стоимость билета или произвольно заданное неотрицательное число. Тем не менее, во всех этих случаях, Dij будет называться расстоянием.
0 | 1 | 2 | 3 | 4 | |
0 | - | 5 | 8 | 9 | 2 |
1 | 5 | - | 3 | 3 | 4 |
2 | 8 | 3 | - | 7 | 6 |
3 | 9 | 3 | 7 | - | 1 |
4 | 2 | 4 | 6 | 1 | - |
Искомый путь замкнут, поэтому любой город можно выбрать в качестве начального (и конечного). Пусть таковым будет нулевой город. Тогда любая перестановка чисел от 1 до 4, окруженная нулями, символизирует некоторый путь, проходящий через каждый город один раз. Например, 0,1,3,2,4,0, означает, что стартуя из города 0, мы перемещаемся в город 1, затем в город 3 и т.д. На последнем шаге из города 4 возвращаемся в стартовую точку - город 0. Длина этого пути равна 5+3+7+6+2 = 23 и он не самый короткий. В данном случае есть две перестановки длиной 17. Это 0,2,1,3,4,0 и 0,4,3,1,2,0. На самом деле это один и тот же путь, который проходится в "прямом" или "обратном" направлении. Подобные перестановки, отличающиеся обращением пути, считаются тождественными. В дальнейшем финальный город будет опускаться и подразумевается, что он всегда совпадает со стартовым. Таким образом, на приведенном выше графе существует единственное оптимальное решение 0,2,1,3,4 с длиной пути равной 17.
Пусть один из N городов фиксирован. Тогда остальные N-1 городов можно переставить (N-1)! способами. Половина из них является обращением пути (циклической перестановкой). Поэтому существует (N-1)!/2 вариантов различных путей, среди которых необходимо найти хотя бы один путь минимальной длины.
Если расстояния между любыми тремя городами i,j и k удовлетворяют неравенству треугольника:
Dij + Djk ≥ Dik,задачу коммивояжёра называют метрической. Её частным случаем является задача соединения замкнутой ломаной линией точек на плоскости ("дороги" между городами считаются прямыми с евклидовым расстоянием в качестве их длины). Ниже приведены решения такой задачи для 20 и 50 городов:
Факториал в выражении (N-1)!/2 растёт очень быстро и уже при N > 15 перебрать все пути проблематично. Поэтому задача коммивояжёра служит хорошей моделью для тестирования непереборных алгоритмов, обладающих "зачатками интеллекта". Эти алгоритмы содержат те или иные эвристические соображения, делающие поиск решения более направленным (осмысленным). Однако в этом документе рассматривается метод "грубой силы", т.е. перебор. В дальнейшем мы неоднократно вернёмся к задаче коммивояжёра.
Генерация перестановок
Теперь перейдём к вычислению расстояния.
Запускаем перебор
Определим массив dists[j][i] расстояний между городами j и i. Для простоты, будем задавать расстояния только в одну сторону, а обратные расстояния установим в цикле:
var dists = [ [ ], // 0 [5 ], // 1 [8, 3 ], // 2 [9, 3, 7 ], // 3 [2, 4, 6, 1], // 4 ]; for ( var j=0; j < dists.length; j++){ // бежим по строкам вниз dists[j][j]=-1; // диагональные элементы for ( var i=j+1; i < dists.length; i++) // вправо от диагонали dists[j][i]=dists[i][j]; // заполняем "дырки" в матрице } |
Двумерный массив
0,1,2,3,4 dist=17
0,1,2,4,3 dist=23
0,1,3,2,4 dist=22
0,1,3,4,2 dist=22
0,1,4,3,2 dist=24
0,1,4,2,3 dist=30
0,2,1,3,4 dist=16
0,2,1,4,3 dist=24
0,2,3,1,4 dist=23
0,2,3,4,1 dist=24
0,2,4,3,1 dist=22
0,2,4,1,3 dist=29
0,3,2,1,4 dist=24
0,3,2,4,1 dist=30
0,3,1,2,4 dist=22
0,3,1,4,2 dist=29
0,3,4,1,2 dist=24
0,3,4,2,1 dist=23
0,4,2,3,1 dist=22
0,4,2,1,3 dist=22
0,4,3,2,1 dist=17
0,4,3,1,2 dist=16
0,4,1,3,2 dist=23
0,4,1,2,3 dist=24
function dist(ar, dists) { var d = dists[0][ar[0]] + dists[ar[ar.length-1]][0]; // начало и конец for ( var i=1; i < ar.length; i++) d += dists[ar[i-1]][ar[i]]; // между ar[i-1] и ar[i] return d; // длина пути перестановки ar } function print(ar, d){ document.write(ar, ' dist=' +d+ '<br>' ); } ar = new Array(dists.length); // перестановки городов 1,...,n-1 for ( var i = 0; i < ar.length; i++) ar[i]=i; // ar = [0, 1, 2, ...] perm(ar, 1); // запускаем перебор перестановок |
Рекурсивный способ перебора всех путей (перестановок чисел) достаточно прост. Однако для больших N, поиск оптимального решения занимает очень много времени, html - страница будет "подвисать", а мы не увидим процесса вычислений. Поэтому, создадим нерекурсивную функцию получения перестановок, а затем, "включим" таймер, и в каждом его "тике" будем проводить только часть вычислений, выводя промежуточные результаты поиска кратчайшего пути.
Не рекурсивный способ
Будем порождать перестановки в лексикографическом порядке. Например: (1,2,3) (1,3,2) (2,1,3) (2,3,1) (3,1,2) (3,2,1). Этот порядок означает, что перестановка (a1,a2,...,an) меньше перестановки (b1,b2,...,bn), если существует такое k ≥ 1, что все ai == bi для i < k и ak < bk. Другими словами начала перестановок совпадают и меньше та, у которой меньше первое не совпадающее число.
Цикл do ... while
Создаём таймер
Чтобы создать таймер, запускаемый нажатием кнопки, вставим в html страницу следующий код:
< input id = "btnID" type = "button" value = "run" onclick = "run(this)" style = "width:4em" > < b id = "outID" style = "font-family:monospace; color:green;" >Нажми на кнопку</ b > |
Вторая строка - это "жирный текст" (окружённый тегами <b>...</b>). Этот текст ("внутренность" между тегами), как и кнопка, имеет собственное имя (id="outID"). Для красоты, при помощи стиля, для текста задаётся моноширинный шрифт и зелёный цвет.
Определим функцию run, вызываемую при нажатии на кнопку:
var timerID; // номер таймера function run(btn) { if (timerID === undefined ){ timerID = setInterval(timer, 100); // создаём таймер на 100 ms, в timerID - его номер btn.value = "stop" ; // меняем надпись на кнопке } else { timerID = clearInterval(timerID); // убиваем таймер, timerID будет undefined btn.value = "run" ; // меняем надпись на кнопке } } |
setInterval
clearInterval
Осталось написать собственно функцию timer и её окружение:
var ar = new Array(6); // массив из 6-ти элементов for ( var i = 0; i < ar.length; i++) ar[i]=i; // в начале ar = 0,1,2,3,4,5 var count = 0; // количество просмотренных перестановок function timer() { var num = 1; // количество итераций за один "тик" таймера do { count++; // тут обрабатываем перестановку document.getElementById( 'outID' ).innerHTML = count + ": " + ar; } while (permNxt(ar, 1) && --num); // 0-город фиксирован, остальные меняем местами if (num){ // while был прерван permNxt timerID = clearInterval(timerID); // убиваем таймер, timerID будет undefined document.getElementById( 'btnID' ).value= "run" // меняем надпись на кнопке } } |
Воспроизводимые случайные числа
Math.random()
function rand(n) { return Math.floor(Math.random()*n); } |
var lcg = ( function () { var z=1, a=1664525, b=1013904223, m = 4294967296; // = 2^32 return { seed : function (val) { z = val || Math.round(Math.random() * m); }, rand : function (bound) { return (z = (a*z + b) % m) % bound; }, random : function () { return (z = (a*z + b) % m) / m; } }; }()); |
lcg.seed(10); // задаём seed 10 for ( var i=50; i--; ) document.write(lcg.rand(10), ',' ); document.write( '<br>' ); lcg.seed(); // задаём случайный seed for ( var i=50; i--; ) document.write(lcg.rand(10), ',' ); |
3,6,5,8,5,4,5,4,3,8,7,2,7,0,3,4,1,4,9,6,3,8,9,8,9,0,9,0,1,4,9,8,1,2,5,4,5,0,9,6,1,6,7,4,3,0,1,0,3,6,Стоит несколько раз обновить страницу. Первая строка меняться не будет, тогда как вторая каждый раз даёт новый набор случайных чисел.
2,1,8,3,4,9,2,5,8,3,6,7,2,9,4,3,8,3,0,1,6,7,6,9,2,3,4,9,0,5,8,5,4,3,4,5,4,3,4,3,8,5,8,9,8,3,2,9,8,7,
Теперь немного о сути кода. В JavaScript любая функция хранится в некоторой памяти, на которую можно получить ссылку. Например, возможен следующий скрипт:
var sqr = function (x) { return x*x; } document.write( sqr(2) ); |
Функции как указатели
Логическое "или": a||b
Матрица расстояний
Создадим матрицу расстояний для произвольного числа городов. Для наглядности будем рассматривать метрическую задачу. Города случайным образом расположим на карте шириной w, высотой h и попарно соединим прямыми дорогами. Кроме кнопки с надписью "Создать", на html-страницу добавим 4 поля редактирования (стили опускаем):
< input type = "button" value = "Создать" onClick = "create()" > <!-- кнопка запуска --> < input type = "text" value = "12" id = "nID" > <!-- число городов --> < input type = "text" value = "256" id = "wID" > <!-- ширина карты --> < input type = "text" value = "256" id = "hID" > <!-- высота карты --> < input type = "text" value = "1" id = "seedID" > <!-- номер последовательности --> |
Объявим следующие глобальные переменные, которые потребуются для вычислений:
var dists = []; // матрица расстояний var path = []; // путь (массив перестановок) var minWay =[], minDist = Infinity, maxDist=0; // лучший путь, его длина и максимальная длина var countPath = 0; // число просмотренных путей var countSol = 0; // число оптимальных решений var timerPathID; // номер таймера var timeTot, timeBest; // общее время и время до лучшего решения var total; // число перестановок var pos; // массив координат городов |
Ниже приведена функция, создающая случайно расположенные города и вычисляющая евклидовы расстояния между ними:
function create() { var n = Number(document.getElementById( 'nID' ).value); // число городов var w = Number(document.getElementById( 'wID' ).value); // ширина карты var h = Number(document.getElementById( 'hID' ).value); // высота карты var seed = Number(document.getElementById( 'seedID' ).value); // номер случайной последовательности pos = new Array(n); lcg.seed(seed); // инициализируем случайную последовательность for ( var j = 0; j < n; j++) // для каждого города pos[j] = { x:lcg.rand(w), y:lcg.rand(h) }; // задаём случайное положение dists = new Array(n); // массив расстояний for ( var j=0; j < n; j++) dists[j] = new Array(n); // расстояния к каждому городу от j-того for ( var j = 0; j < n; j++){ dists[j][j] = -1; // диагональные элементы for ( var i = 0; i < j; i++ ){ var dx = pos[i].x - pos[j].x; // вычисляем евклидово расстояние var dy = pos[i].y - pos[j].y; dists[j][i] = dists[i][j] = Math.sqrt(dx*dx+dy*dy); } } minDist = Infinity; maxDist = 0; // наименьшая и наибольшая длина пути countPath = 0; countSol = 0; // число просмотренных путей и оптимальных решений path = new Array(dists.length); // собственно путь for ( var i=0; i < path.length; i++) path[i]=i; // 0,1,2,3,... timeTot = timeBest = 0; // общее время и время до лучшего решения total = fac(n-1); // число перестановок (n-1)! (Recursion.html) } |
Обратим внимание, что свойство value текстового поля является строкой, поэтому к нему применяется функция Number, которая переводит строку в число. Кроме этого расстояние между городами округляется (Math.round), поэтому длина пути всегда будет целым числом.
Рисуем на канвасе
Чтобы нарисовать города и путь между ними, добавим в html-страницу тег канваса:
< canvas id = "canID" width = "10" height = "10" ></ canvas > |
function show(pos, path, w, h) { var canvas = document.getElementById( 'canID' ); // получаем объект канваса var ctx = canvas.getContext( '2d' ); // у него есть свойство - контекст рисования var x0 = 10, y0 = 10; // положение левого верхнего угла карты canvas.width = w+2*x0; canvas.height = h+2*y0; // меняем размеры канваса (чуть больше, чем w x h) ctx.beginPath(); // начало рисования ломаной линии ctx.moveTo(x0+pos[path[0]].x,y0+pos[path[0]].y) // переходим на 0-й город for ( var i=1; i < path.length; i++) // рисуем отрезки пути к i-тому городу ctx.lineTo(x0+pos[path[i]].x, y0+pos[path[i]].y); ctx.closePath(); // соединяем последнюю и первую точку ctx.stroke(); // собственно рисуем линию ctx.fillStyle = '#FFA' ; // цвет заливки круга for ( var i=0; i < pos.length; i++){ ctx.beginPath(); ctx.arc(x0+pos[i].x, y0+pos[i].y, 10, 0, Math.PI*2, true ); ctx.stroke(); // рисуем окружность ctx.fill(); // рисуем круг } ctx.font = "12pt Consolas" ; // моноширинный шрифт ctx.textAlign = "center" ; // текст центрован по горизонтали ctx.textBaseline = "middle" ; // и по вертикали ctx.fillStyle = '#000' ; // цвет текста for ( var i=0; i < pos.length; i++) ctx.fillText(i, x0+pos[i].x, y0+pos[i].y); // выводим текст (номер города) } |
Перебор в таймере
Реализация переборного алгоритма поиска кратчайшего пути в таймере не представляет труда. Для сохранения лучшего пути в массиве minWay, напишем функцию копирования значений элементов массива src в массив des:
function copy(des, src) { if (des.length !== src.length) des = new Array(src.length); for ( var i=0; i < src.length; i++) des[i] = src[i]; return des; } |
assign
Обратим внимание на ещё один момент. Функция copy возвращает ссылку на копируемый массив, которую мы присваиваем в minWay. Если это не сделать, то при первом её вызове, когда minWay=[], произойдёт выделение памяти под новый массив (в if-ниже), о чём minWay ни чего не знает. Записывая minWay = copy(minWay, path) мы устраняем эту неприятность.
Функция, вызываемая в таймере имеет вид:
function timerPath() { var time = window.performance.now(); // время в начале функции var num = 1000000; // количество итераций за один "тик" таймера do { countPath++; // тут обрабатываем перестановку var d = dist(path, dists); // вычисляем длину пути if (d === minDist) countSol++; // число оптимальных решений if (d < minDist){ // нашли более короткий путь minDist = d; // запоминаем кратчайшее расстояние minWay = copy(minWay, path); // запоминаем путь countSol = 1; // первое оптимальное решение timeBest = timeTot; // время до этого решения } if (d > maxDist) maxDist = d; // запоминаем максимальное расстояние } while (permNxt(path, 1) && --num); // пока есть перестановки и итерации timeTot += window.performance.now() - time; // время вычислений document.getElementById( 'outPathID' ).innerHTML = "minDist: " + minDist.toFixed(2) + ", path: " + minWay + "<br>time: " + timeBest.toFixed(0) + " ms, " + "maxDist = " + maxDist.toFixed(2) + ", countSol = " + countSol + ", total: " + timeTot.toFixed(0) + " ms, " + "iters: " +(100*countPath/total).toFixed(2) + "%" ; if (num){ // while был прерван permNxt timerPathID = clearInterval(timerPathID); // убиваем таймер, timerPathID будет undefined document.getElementById( 'btnPathID' ).value= "run" ; } } |
Результат
Ниже, при нажатии кнопки "Создать", генерится массив расстояний между городами. Их число, ширина, высота карты и "номер" последовательности можно менять. Результат представлен svg-картинкой и таблицей. Для запуска полного перебора, нажимаем на кнопку "run":
городов на карте w = , h = и seed = , показывать распределение длинполный перебор
Нажми на кнопку
Распределение (в процентах) вариантов различных длин (доля путей с данной длиной):
В браузере Google Chrome, на процессоре Intel Core i5-2300 2.8 GHz, 4 Gb
полный перебор занимает следующее чистое время (без учёта вывода промежуточных результатов):
N | 10 | 11 | 12 | 13 | 14 | 15 | ... | 20 |
---|---|---|---|---|---|---|---|---|
time, tN | 41 ms | 331 ms | 4s | 48 s | 12 m | 3 h | ~ 600 лет | |
tN/tN-1 | 8.1 | 11.4 | 12.7 | 15.2 | 14.7 |
Понятно, что задача с всего лишь 20-ю городами, имеющая 6·1016 вариантов путей, методом перебора не решается и необходимо использовать другие подходы, которые будут рассмотрены в дальнейшем.