diff --git a/src/index.html b/src/index.html index a1dfd6f..3670874 100644 --- a/src/index.html +++ b/src/index.html @@ -51,10 +51,12 @@

Для планирования маршрута выберите регион, и точки для посещения. Затем разместите в начале списка пункт с которого планируете начать - путешествие, а в конце, пункт в котором планируете закончить. После чего нажмите кнопку - + путешествие. После чего нажмите кнопку - Рассчитать маршрут.

+
+
diff --git a/src/js/hamiltonPath.js b/src/js/hamiltonPath.js index d75b1ba..a830633 100644 --- a/src/js/hamiltonPath.js +++ b/src/js/hamiltonPath.js @@ -874,4 +874,106 @@ function hamiltonianCycle(graph) { // Return found cycles. return cycles; +} + + +function findAllPaths(startVertex, paths = [], path = []) { + // Clone path. + const currentPath = [...path]; + + // Add startVertex to the path. + currentPath.push(startVertex); + + // Generate visited set from path. + const visitedSet = currentPath.reduce((accumulator, vertex) => { + const updatedAccumulator = { + ...accumulator + }; + updatedAccumulator[vertex.getKey()] = vertex; + + return updatedAccumulator; + }, {}); + + // Get all unvisited neighbors of startVertex. + const unvisitedNeighbors = startVertex.getNeighbors().filter((neighbor) => { + return !visitedSet[neighbor.getKey()]; + }); + + // If there no unvisited neighbors then treat current path as complete and save it. + if (!unvisitedNeighbors.length) { + paths.push(currentPath); + + return paths; + } + + // Go through all the neighbors. + for (let neighborIndex = 0; neighborIndex < unvisitedNeighbors.length; neighborIndex += 1) { + const currentUnvisitedNeighbor = unvisitedNeighbors[neighborIndex]; + findAllPaths(currentUnvisitedNeighbor, paths, currentPath); + } + + return paths; +} + +/** + * @param {number[][]} adjacencyMatrix + * @param {object} verticesIndices + * @param {GraphVertex[]} cycle + * @return {number} + */ +function getCycleWeight(adjacencyMatrix, verticesIndices, cycle) { + let weight = 0; + + for (let cycleIndex = 1; cycleIndex < cycle.length; cycleIndex += 1) { + const fromVertex = cycle[cycleIndex - 1]; + const toVertex = cycle[cycleIndex]; + const fromVertexIndex = verticesIndices[fromVertex.getKey()]; + const toVertexIndex = verticesIndices[toVertex.getKey()]; + weight += adjacencyMatrix[fromVertexIndex][toVertexIndex]; + } + + return weight; +} + +/** + * BRUTE FORCE approach to solve Traveling Salesman Problem. + * + * @param {Graph} graph + * @return {GraphVertex[]} + */ +function bfTravellingSalesman(graph) { + // Pick starting point from where we will traverse the graph. + const startVertex = graph.getAllVertices()[0]; + + // BRUTE FORCE. + // Generate all possible paths from startVertex. + const allPossiblePaths = findAllPaths(startVertex); + + // Filter out paths that are not cycles. + const allPossibleCycles = allPossiblePaths.filter((path) => { + /** @var {GraphVertex} */ + const lastVertex = path[path.length - 1]; + const lastVertexNeighbors = lastVertex.getNeighbors(); + + return lastVertexNeighbors.includes(startVertex); + }); + + // Go through all possible cycles and pick the one with minimum overall tour weight. + const adjacencyMatrix = graph.getAdjacencyMatrix(); + const verticesIndices = graph.getVerticesIndices(); + let salesmanPath = []; + let salesmanPathWeight = null; + for (let cycleIndex = 0; cycleIndex < allPossibleCycles.length; cycleIndex += 1) { + const currentCycle = allPossibleCycles[cycleIndex]; + const currentCycleWeight = getCycleWeight(adjacencyMatrix, verticesIndices, currentCycle); + + // If current cycle weight is smaller then previous ones treat current cycle as most optimal. + if (salesmanPathWeight === null || currentCycleWeight < salesmanPathWeight) { + salesmanPath = currentCycle; + salesmanPathWeight = currentCycleWeight; + } + } + + // Return the solution. + return salesmanPath; } \ No newline at end of file diff --git a/src/js/index.js b/src/js/index.js index 39949d2..f1aecda 100644 --- a/src/js/index.js +++ b/src/js/index.js @@ -192,19 +192,85 @@ function calculateDistance(lat1, lon1, lat2, lon2) { return d; } +function shuffle(array) { + let currentIndex = array.length; + + // While there remain elements to shuffle... + while (currentIndex != 0) { + + // Pick a remaining element... + let randomIndex = Math.floor(Math.random() * currentIndex); + currentIndex--; + + // And swap it with the current element. + [array[currentIndex], array[randomIndex]] = [ + array[randomIndex], array[currentIndex] + ]; + } +} + +function showTravelPoints(indexes) { + let calculatedPoints = []; + let prevPoint = null; + let id = 0; + for (const i in indexes) { + const idx = indexes[i]; + const point = points[idx]; + + if (prevPoint != null) { + const d = (calculateDistance(point.location[0], point.location[1], prevPoint.location[0], prevPoint.location[1]) / 1000.0).toFixed(2); + calculatedPoints.push({ + ID: id, + Waypoint: d + ' км.' + }); + id++; + } + + calculatedPoints.push({ + ID: id, + Waypoint: point.text + }); + id++; + + prevPoint = point; + } + $(() => { + $('#waypointsGrid').dxDataGrid({ + dataSource: calculatedPoints, + showColumnHeaders: false, + showColumnLines: false, + showRowLines: true, + rowAlternationEnabled: true, + keyExpr: 'ID', + columns: ['Waypoint'], + showBorders: true, + }); + }); +} + // Вычисление оптимального маршрута по начальной и конечной точке function recalculateWaypoints(waypoints) { - if (!waypoints || waypoints.length < 2) { + if (!waypoints || waypoints.length < 1) { DevExpress.ui.notify("Недостаточно точек для построения маршрута"); + } else if (waypoints.length == 1) { + + // Одна точка, просто ее выводим + showTravelPoints([waypoints[0].id]); } else { var start = waypoints[0]; var end = waypoints[waypoints.length - 1]; DevExpress.ui.notify("Расчет маршрута от '" + start.text + "' к '" + end.text + "'"); if (waypoints.length == 2) { + // Две точки, старт и стоп, выводим без расчета + showTravelPoints([waypoints[0].id, waypoints[1].id]); } else if (waypoints.length == 3) { - } else { + // Три точки, старт, стоп и промежуточная выводим без расчета + showTravelPoints([waypoints[0].id, waypoints[1].id, waypoints[2].id]); + } else if (waypoints.length <= 9) { + + // Если точек от 4 до 9, используем алгоритм коммивояжера // Создаем словарь с вершинами let vertexes = {}; for (const ind in waypoints) { @@ -221,9 +287,17 @@ function recalculateWaypoints(waypoints) { graph.addEdge(new GraphEdge(vertexes[i], vertexes[j], d)); } } - // Вычисляем Гамильтонов цикл для графа - const hamiltonianCycleSet = hamiltonianCycle(graph); - alert(hamiltonianCycleSet.length); + // Решение задачи коммивояжера + const salesmanPath = bfTravellingSalesman(graph); + let indexes = []; + for (var ind in salesmanPath) { + indexes.push(salesmanPath[ind].value); + } + showTravelPoints(indexes); + } else { + // Если точек от 9, близкие к np алгоритмам расчеты слишком затратные + // применяем лучшие поиск из 200.000 лучших вариантов (9! = 362880, берем такого порядка число чтобы не грузить браузер расчетами) + } } }