路径算法(二)

608 阅读6分钟

这是我参与8月更文挑战的第7天,活动详情查看: 8月更文挑战

Dijkstra 算法

宽度优先搜索算法,解决了起始顶点到目标顶点路径规划问题,但不是最优以及合适的,因为它的边没有权值(比如距离),路径无法进行估算比较最优解。为何权值这么重要,因为真实环境中,2个顶点之间的路线并非一直都是直线,需要绕过障碍物才能达到目的地,比如森林,湖水,高山,都需要绕过而行,并非直接穿过。

比如我采用宽度优先算法,遇到如下情况,他会直接穿过障碍物(绿色部分 ),明显这个不是我们想要的结果:

image.png

解决痛点:

寻找图中一个顶点到另一个顶点的最短以及最小带权路径是非常重要的提炼过程。为每个顶点之间的边增加一个权值,用来跟踪所选路径的消耗成本,如果位置的新路径比先前的最佳路径更好,我们将添加它,规划到新的路线中。

Dijkstra 算法基于宽度优先算法进行改进,把当前看起来最短的边加入最短路径树中 ,利用贪心算法计算并最终能够产生最优结果的算法。具体步骤如下:

  1. 每个顶点都包含一个预估值cost(起点到当前顶点的距离),每条边都有权值v ,初始时,只有起始顶点的预估值cost为0,其他顶点的预估值d都为无穷大 ∞。

  2. 查找cost值最小的顶点A,放入path队列

  3. 循环A的直接子顶点,获取子顶点当前cost值命名为current_cost,并计算新路径new_cost,new_cost=父节点A的cost v(父节点到当前节点的边权值),如果new_cost<current_cost,当前顶点的cost=new_cost

  4. 重复2,3直至没有顶点可以访问.

我们看下图例:

bfs_dijkstra.gif

我们看下代码:

// Predecessor map for each node that has been encountered.
// node ID => predecessor node ID
var predecessors = {};

// Costs of shortest paths from s to all nodes encountered.
// node ID => cost
var costs = {};
costs[s] = 0;

// Costs of shortest paths from s to all nodes encountered; differs from
// `costs` in that it provides easy access to the node that currently has
// the known shortest path from s.
// XXX: Do we actually need both `costs` and `open`?
var open = dijkstra.PriorityQueue.make();
open.push(s, 0);

var closest,
  u, v,
  cost_of_s_to_u,
  adjacent_nodes,
  cost_of_e,
  cost_of_s_to_u_plus_cost_of_e,
  cost_of_s_to_v,
  first_visit;
while (!open.empty()) {
  // In the nodes remaining in graph that have a known cost from s,
  // find the node, u, that currently has the shortest path from s.
  closest = open.pop();
  u = closest.value;
  cost_of_s_to_u = closest.cost;

  // Get nodes adjacent to u...
  adjacent_nodes = graph[u] || {};

  // ...and explore the edges that connect u to those nodes, updating
  // the cost of the shortest paths to any or all of those nodes as
  // necessary. v is the node across the current edge from u.
  for (v in adjacent_nodes) {
    if (adjacent_nodes.hasOwnProperty(v)) {
      // Get the cost of the edge running from u to v.
      cost_of_e = adjacent_nodes[v];

      // Cost of s to u plus the cost of u to v across e--this is *a*
      // cost from s to v that may or may not be less than the current
      // known cost to v.
      cost_of_s_to_u_plus_cost_of_e = cost_of_s_to_u + cost_of_e;

      // If we haven't visited v yet OR if the current known cost from s to
      // v is greater than the new cost we just found (cost of s to u plus
      // cost of u to v across e), update v's cost in the cost list and
      // update v's predecessor in the predecessor list (it's now u).
      cost_of_s_to_v = costs[v];
      first_visit = (typeof costs[v] === 'undefined');
      if (first_visit || cost_of_s_to_v > cost_of_s_to_u_plus_cost_of_e) {
        costs[v] = cost_of_s_to_u_plus_cost_of_e;
        open.push(v, cost_of_s_to_u_plus_cost_of_e);
        predecessors[v] = u;
      }
    }
  }
}

我们看到虽然Dijkstra 算法 虽然相对于宽度优先搜索更加智能,可以规避路线比较长或者无法行走的区域,但依然会存在盲目搜索的倾向,我们在地图中常见的情况是查找目标和起始点的路径,具有一定的方向性,而Dijkstra 算法从上述的图中可以看到,也是基于起点向子节点全方位扩散。

缺点:

  1. 运行时间复杂度是:T(n) = O(V^2),其中V为顶点个数。效率上并不高\

  2. 目标查找不具有方向性

如何解决让搜索不是全盘盲目瞎找?我们来看Greedy Best First Search算法(贪婪最佳优先搜索)。

贪婪最佳优先搜索(Greedy Best First Search)

在Dijkstra算法中,我已经发现了其最终要的缺陷,搜索存在盲目性。在这里,我们只针对这个痛点,采用贪婪最佳优先搜索来解决。如何解决?我们只需稍微改变下观念即可,在Dijkstra算法中,优先队列采用的是,每个顶点到起始顶点的预估值来进行排序。在贪婪最佳优先搜索中

解决痛点:

我们采用每个顶点到目标顶点的距离进行排序。一个采用离起始顶点的距离来排序,一个采用离目标顶点距离排序(离目标的远近排序)

哪个更快?我们看下图(左边宽度优先,右边贪婪优先):

bfs_gbfs.gif

从上图中我们可以明显看到右边的算法(贪婪最佳优先搜索 )寻找速度要快于左侧,虽然它的路径不是最优和最短的,但障碍物最少的时候,他的速度却足够的快。这就是贪心算法的优势,基于目标去搜索,而不是完全搜索。

我们看下算法:

let unvisitedNodes = []; //open list
let visitedNodesInOrder = []; //closed list
startNode.distance = 0;
unvisitedNodes.push(startNode);

while (unvisitedNodes.length !== 0) {
  unvisitedNodes.sort((a, b) => a.totalDistance - b.totalDistance);
  let closestNode = unvisitedNodes.shift();
  if (closestNode === finishNode) return visitedNodesInOrder;

  closestNode.isVisited = true;
  visitedNodesInOrder.push(closestNode);

  let neighbours = getNeighbours(closestNode, grid);
  for (let neighbour of neighbours) {
    let distance = closestNode.distance + 1;
    //f(n) = h(n)
    if (neighbourNotInUnvisitedNodes(neighbour, unvisitedNodes)) {
      unvisitedNodes.unshift(neighbour);
      neighbour.distance = distance;
      neighbour.totalDistance = manhattenDistance(neighbour, finishNode);
      neighbour.previousNode = closestNode;
    } else if (distance < neighbour.distance) {
      neighbour.distance = distance;
      neighbour.totalDistance = manhattenDistance(neighbour, finishNode);
      neighbour.previousNode = closestNode;
    }
  }
}

function manhattenDistance(node, finishNode) {
  let x = Math.abs(node.row - finishNode.row);
  let y = Math.abs(node.col - finishNode.col);
  return x + y;
}

缺点:

路径不是最短路径,只能是较优

如何在搜索尽量少的顶点同时保证最短路径?我们来看A*算法。

A*算法(A-Star)

从上面算法的演进,我们逐渐找到了最短路径和搜索顶点最少数量的两种方案,Dijkstra 算法和 贪婪最佳优先搜索。那么我们有没有可能汲取两种算法的优势,令寻路搜索算法即便快速又高效?

答案是可以的,A*算法正是这么做了,它吸取了Dijkstra 算法中的cost_so_far,为每个边长设置权值,不停的计算每个顶点到起始顶点的距离,以获得最短路线,同时也汲取贪婪最佳优先搜索算法中不断向目标前进优势,并持续计算每个顶点到目标顶点的距离,以引导搜索队列不断想目标逼近,从而搜索更少的顶点,保持寻路的高效。

解决痛点:

A*算法的优先队列排序方式基于F值:

F=cost(顶点到起始顶点的距离 ) heuristic(顶点到目标顶点的距离 )

以下分别是Dijkstra算法,贪心算法,以及A*算法的寻路雷达图,其中格子有数字标识已经被搜索了,可以对比下三种效率:

image.png

B*算法(Branch Star)

B算法是一种比A算法更高效的算法, 适用于游戏中怪物的自动寻路,其效率远远超过A算法,经过测试,效率是普通A算法的几十上百倍。B*算法不想介绍了,自己去google下吧。

通过以上算法不断的演进,我们可以看出每一种算法的局限,以及延伸出的新算法中出现的解决方式,希望方便你的理解。

image.png