单源最短路径

数据结构可视化

dijkstra

算法描述

  • 解决:正权重连通图中,源节点与其他节点的最短距离
  • 应用:GPS寻路

历史背景

荷兰杰出计算机科学家、软件工程师 Dr. Edsger W. Dijkstra 创建并发布了这个算法。
123
1994 年,Edsger Dijkstra 博士 在 ETH Zurich(Andreas F. Borchert 摄)

  • 博士的采访:

从 Rotterdam 到 Groningen 的最短路线是什么?我花了大概 20 分钟时间设计了这个寻找最短路径的算法。一天早上我正和我年轻的未婚妻在 Amsterdam 逛街,觉得有点累了,我们就坐在咖啡厅的露台上喝了一杯咖啡,我在想是否能够解决这个问题,然后,我设计出了这个最短路径算法。我说过,这是一个 20 分钟的设计。事实上,三年之后的 1959 年它才被发布,现在看来依然很不错,其原因之一是我当时设计的时候没有纸和笔,从而不得不极力避免所有可避免的复杂性。最终,令我惊讶的是,这个算法成为了我成名的基石之一。——引自文章《An interview with Edsger W. Dijkstra》.

看来生活中善于思考的人,不仅仅能取得更多成就,还能提升幸福感。仅仅 20 分钟的时间,Dijkstra 博士设计出了位列计算机科学史上最著名的算法之一的 Dijkstra 算法。

算法过程

  • 寻找源节点最近节点(贪心思想)
  • 记录已知最短路径,有更短的路径时更新,标记已访问
  • 重复操作

必要条件

权重为正

有负边,会导致之前保存的最短距离无效,出现走的多但是距离短,不切实际。

C++实现(堆优化)

#include <bits/stdc++.h>
#define INF 0x3f3f3f3f // 定义一个代表无穷大的常量
using namespace std;

typedef pair<int, int> iPair;  //边的终点和权重

/*
 * 功能: 添加边到图的邻接表中
 * 参数: adj - 邻接表,存储图的边
 *        u - 第一端点
 *        v - 第二端点
 *        w - 边的权重
 */
void addEdge(vector<pair<int, int> > adj[], int u, int v, int w){
    // 将边 (u, v) 和权重 w 添加到邻接表中
    adj[u].push_back(make_pair(v, w)); // 从 u 到 v
    adj[v].push_back(make_pair(u, w)); // 从 v 到 u(无向图)
}

/*
 * 功能: 使用Dijkstra算法计算从源点到其他所有顶点的最短路径
 * 参数: adj - 邻接表
 *        V - 顶点数量
 *        src - 源点
 */
void dijkstra(vector<pair<int, int> > adj[], int V, int src){
    // 优先队列,存储当前顶点及其最短路径的权重(堆优化)
    priority_queue<iPair, vector<iPair>, greater<iPair> > pq;
    // 存储从源点到每个顶点的最短距离,初始化为无穷大
    vector<int> dist(V, INF);
    // 访问标记数组,记录哪些顶点已经被访问过
    vector<bool> visited(V, false);

    // 将源点距离初始化为0,并将其加入到优先队列中
    pq.push(make_pair(0, src));
    dist[src] = 0;

    // 当优先队列不为空时
    while(!pq.empty()){
        // 获取当前顶点
        int u = pq.top().second;
        pq.pop();
        
        // 如果该顶点已经访问过,继续下一个
        if(visited[u]) continue;
        visited[u] = true;     // 将顶点标记为已访问
        
        // 遍历当前顶点的所有邻居
        for(auto x : adj[u]){
            int v = x.first; // 邻居顶点
            int wt = x.second; // 边的权重
            
            // 如果通过当前顶点到邻居的路径距离更短
            if(dist[v] > dist[u] + wt){
                dist[v] = dist[u] + wt; // 更新最短路径
                pq.push(make_pair(dist[v], v));  // 将更新后的邻居顶点加入到优先队列中
            }
        }
    } 
    
    // 输出每个顶点从源点的最短距离
    printf("Vertex distance from source\n");
    for(int i = 0; i < V; i++){
        printf("%d \t\t %d\n", i, dist[i]); // 输出顶点编号及其距离
    }
}

/*
 * 主函数: 程序的入口点
 */
int main() {
    int V = 9; // 顶点的数量
    vector<iPair> adj[V]; // 定义邻接表,用于存储图

    // 添加边到邻接表
    addEdge(adj, 0, 1, 4);
    addEdge(adj, 0, 7, 8);
    addEdge(adj, 1, 2, 8);
    addEdge(adj, 1, 7, 11);
    addEdge(adj, 2, 3, 7);
    addEdge(adj, 2, 8, 2);
    addEdge(adj, 2, 5, 4);
    addEdge(adj, 3, 4, 9);
    addEdge(adj, 3, 5, 14);
    addEdge(adj, 4, 5, 10);
    addEdge(adj, 5, 6, 2);
    addEdge(adj, 6, 7, 1);
    addEdge(adj, 6, 8, 6);
    addEdge(adj, 7, 8, 7);

    // 从源点0开始进行Dijkstra算法计算
    dijkstra(adj, V, 0);
     
    return 0; // 程序结束
}  

Bellman–Ford

算法介绍

对连通图,寻找源点与其他节点最短距离,有负环返回false
通过N-1次对所有边遍历松弛,得到最短距离。因为最短距离取得时的最坏情况是源点通过N-1条边抵达目标点,所以N-1次对所有边遍历松弛后必然得到与所有点的最短路径。
dijkstra 算法比较:

  • dijkstra 算法不能处理负权重问题,Bellman–Ford
  • dijkstra 算法贪心遍历,有负值时旧数据无法更新;Bellman–Ford算法每次进行数据更新,可以处理负权值情况,但随之时间复杂度更高

算法实现

伪代码

procedure BellmanFord(list vertices, list edges, vertex source)
   // 读入边和节点的列表 
   // 初始化图 distance为∞,predecessor为空
   for each vertex v in vertices:
       if v is source then distance[v] := 0
       else distance[v] := infinity
       predecessor[v] := null
   // 对所有节点
   for i from 1 to size(vertices)-1:
        //检查每条边
       for each edge (u, v) with weight w in edges:
           if distance[u] + w < distance[v]:
               distance[v] := distance[u] + w
               predecessor[v] := u

   // 检查是否有负权重的回路
   for each edge (u, v) with weight w in edges:
       if distance[u] + w < distance[v]:
           error "图包含负权重的回路"  

C++实现

#include <bits/stdc++.h>
using namespace std;
// Define structure for edges
struct Edge
{
    int u, v, w;
};

bool BellmanFord(int V, vector<Edge> &edges, int src){
    // Initialize distances from source to all other vertices as infinity
    vector<int> dist(V, INT_MAX);
    dist[src] = 0;
    // Relax all edges |V-1| times
    for(int i=0; i<V-1; i++){
        for(Edge e : edges){
            if(dist[e.u] != INT_MAX && dist[e.v] > dist[e.u] + e.w){
                dist[e.v] = dist[e.u] + e.w;
            }
        }
    }
    // Check for negative cycles
    for(Edge e : edges){
        if(dist[e.u] != INT_MAX && dist[e.v] > dist[e.u] + e.w){
            return false; //Negative cycle detected
        }
    }
    return true;
}

int main(){
    int V = 5;
    vector<Edge> edges = {{0, 1, -1}, {0, 2, 4}, {1, 2, 3}, {1, 3, 2}, {1, 4, 2}, {3, 2, 5}, {3, 1, 1}, {4, 3, -3}};
    int src = 0;
    if(BellmanFord(V, edges, src)){
        cout << "No negative cycle found" << endl;
    }
    else{
        cout << "Negative cycle detected" << endl;
    }
    return 0;

}

SPFA

简介

  • Bellman–Ford算法每次更新都重头开始,过于暴力;dijkstra 算法贪心遍历,有负值时旧数据无法更新;SPFA 算法Bellman–Ford算法基础上优化,用队列储存已经,不仅能处理负权图,效率也更高。

队列储存已优化边,如果有能放松且不在队列中的点,则将其入队。

实现

伪码

src, V, edges
for each vertex except for src in Vertex(G):
    d(v) : INF
d(src) : 0
push src into Q
while Q not empty:
    u : front in Q
    pop front in Q
    for edge[u, v] in edges:
        if d(v) > d(u) + w(u, v) 
            then d(v) = d(u) + w(u, v)
            if v not in Q:
                push v into Q  

C++实现(优化版)

优化点在于只取比头结点小的点入队
其他优化策略:

  • 每次正常入队后if d(Q.front) > d(Q.back) then t = Q.pop_back, Q.push_front,保证短距离在前
#include <bits/stdc++.h>  // 引入标准库
using namespace std;

// 定义边的结构体
struct Edge {
    int u, v, w;  // u表示起点,v表示终点,w表示边的权重
};

// SPFA算法实现
void SPFA(vector<Edge>& edges, int V, int Src) {
    vector<int> dist(V, INT_MAX);  // 初始化距离数组,设置为无穷大
    dist[Src] = 0;  // 源点到自身的距离为0
    deque<int> q;  // 使用双端队列来存储节点
    q.push_back(Src);  // 将源点加入队列

    while (!q.empty()) {  // 只要队列不为空
        int u = q.front();  // 取出队首元素
        q.pop_back();  // 弹出队首元素

        // 遍历所有边
        for (Edge e : edges) {
            // 如果通过u到达e.v的距离更短
            if (dist[e.v] > dist[e.u] + e.w) {
                dist[e.v] = dist[e.u] + e.w;  // 更新距离

                // 如果队列不为空且更新的距离比队列中最前面的节点的距离短,则加入队列
                if (!q.empty() && dist[e.v] < dist[q.front()]) 
                    q.push_back(e.v);
            }
        }
    }
}

int main() {
    int V = 5;  // 图中节点的数量
    vector<Edge> edges;  // 存储图的边

    // 添加边
    edges.emplace_back(Edge{0, 1, 4});  // 从0到1,权重为4
    edges.emplace_back(Edge{0, 2, 2});  // 从0到2,权重为2
    edges.emplace_back(Edge{1, 2, 1});  // 从1到2,权重为1
    edges.emplace_back(Edge{1, 3, 3});  // 从1到3,权重为3
    edges.emplace_back(Edge{2, 4, 2});  // 从2到4,权重为2
    edges.emplace_back(Edge{3, 2, 1});  // 从3到2,权重为1
    edges.emplace_back(Edge{3, 4, -3}); // 从3到4,权重为-3(表示负权重)

    SPFA(edges, V, 0);  // 从源点0开始运行SPFA算法

    return 0;  // 程序结束
}  

A*算法

算法简介

A*算法是贪心算法(dijkstra)与启发式算法的综合,贪心算法负责准确性,启发式负责方向指引。

F = G + H

这是核心思想体现,G是距离源点距离(贪心得出),H是距离目的地的估计值(启发式得出)。dijkstra算法H=0,没有方向指引,可以找到,但效率低;启发式算法G=0,只有估计值,不一定能够找到准确的最短路径。

实现

准备

  • 评价函数:F = G + H
    F:距离评价值 G:距离源点距离 H:距离目标点距离

H由启发式函数估计得出,常见的启发式有:欧几里得距离、曼哈顿距离、切夫雪比距离

  • 开放集OPENING、封闭集CLOSED

分别对应两个集合

  • RELAX(松弛操作)
    有源点src, 对于u的邻点v,if Distance(src, v) > Distance(src, u) + Distance(u, v) then Distance(src, v) = Distance(src, u) + Distance(u, v)

重复的周期过程

//预备--tem属于CLOSED,其他节点均既不在OPENING也不在CLOSED。tem是循环内变化点,SRC是源点。

  F: []  
  for neighbor in neighbors of tem: 
      if neighbor not in OPENING:
          push neighbor into OPENING 
          neighbor.father = tem  
      else if neighbor in OPENING:
          //relax-->neighbor加入OPENING后,G = Distance(SRC, neighbor)可能会发生改变。在此通过松弛操作,确保G值最小。
          if Distance(SRC, neighbor) > Distance(src, tem) + Distance(tem, neighbor): 
              Distance(SRC, neighbor) = Distance(src, tem) + Distance(tem, neighbor)
              neighbor.father = tem    //找到destination后据此回溯最短路径
      else if neighbor in CLOSED or other unavailable visiting elements:
          pass  
      h = estimate_function(neighbor, destination)
      g = G = Distance(SRC, neighbor)  
      f = h + g
      push f into F  
  next_tem = F.min()  
  push next_tem into CLOSED   

图示与说明

  1. 现在我设计一款游戏,想找到GREEN到RED的最短路径,中间BLUE是不可访问点。
  2. GREEN作为SRC加入CLOSED(带有浅蓝色轮廓),遍历他的neighbors,根据不在OPENING中的点加入OPENING,father值更新;在OPENING中的点RELAX操作原则,显然都加入OPENING(带有浅绿色轮廓)并更新father值,赋予g,h,f值属性,f加入F集合
  3. 显然右边元素neighbor_right的F值最小,将neighbor_right边框标识浅蓝色(加入CLOSED),neighbor_father赋值为SRC
  4. 重复2.3.直至destination加入CLOSED。
  5. 根据元素father值,依次恢复路径(即最短路径)

参考文章:

posted @ 2024-11-11 22:28  luvmis  阅读(74)  评论(0)    收藏  举报