# 单源最短路问题

# 基本概念

# 初始化

初始化过程定义顶点的最短距离 d[v] , 边权值矩阵 w[u][v] 和前驱数组 pi[v] .

initializeSingleSource
void initializeSingleSource(Graph G, int s) {
    for (int v:G.vertices) {
        d[v] = INT_MAX;
        pi[v] = NULL;
    }
    d[s] = 0; // source
}

# Bellman-Ford 算法

可以解决存在负权边情况下的单源最短路问题。

算法的核心是松弛 (relax) 技术。

relax
int relax(int u,int v) {
    if (d[v] > d[u] + w[u][v]) {
        d[v] = d[u] + w[u][v];
        pi[v] = u;
    }
}

这样,Bellman-Ford 算法可以描述为下述代码:

BellmanFord
bool BellmanFord(Graph G, int s) {
    // initialize
    initializeSingleSource(G, s);
    for (auto v:G.vertices) {
        for (auto e:G.edges) {
            relax(e.from, e.to);
        }
    }
    for (auto e:G.edges) {
        int u = e.from;
        int v = e.to;
        if (d[v] > d[u] + w[u][v]) return false;
    }
    return true;
}

可以看出,Bellman-Ford 算法的复杂度是 O(nm)O(nm) 的。

需要注意的是,以 ss 点为源点跑 Bellman–Ford 算法时,如果没有给出存在负环的结果,只能说明从 ss 点出发不能抵达一个负环,而不能说明图上不存在负环。

因此如果需要判断整个图上是否存在负环,最严谨的做法是建立一个超级源点,向图上每个节点连一条权值为 00 的边,然后以超级源点为起点执行 Bellman–Ford 算法。

# SPFA

SPFA 是队列优化的 Bellman-Ford 算法。最差复杂度仍为 O(nm)O(nm), 伪代码如下:

SPFA
void SPFA(Graph G, int s) {
    initializeSingleSource(G,s);
    initializeQueue(Q);
    Q.push(s);
    
    while (!Q.empty()) {
        u = Q.front();
        Q.pop();
        
        for (auto v : adj[u]) {
            tmp = d[v];
            relax(u,v);
            if (tmp != d[v] && !Q.has(v))
                Q.push(v);
        }
    }
}

# Dijkstra 算法

是一种求解非负权图上单源最短路径的算法。

Dijkstra
struct edge {
  int v, w;
};
vector<edge> e[maxn];
int dis[maxn], vis[maxn];
void Dijkstra(int n, int s) {
  memset(dis, 63, sizeof(dis));
  dis[s] = 0;
  for (int i = 1; i <= n; i++) {
    int u = 0, mind = 0x3f3f3f3f;
    for (int j = 1; j <= n; j++)
      if (!vis[j] && dis[j] < mind) u = j, mind = dis[j];
    vis[u] = true;
    for (auto ed : e[u]) {
      int v = ed.v, w = ed.w;
      if (dis[v] > dis[u] + w) dis[v] = dis[u] + w;
    }
  }
}

这是暴力的写法,复杂度是 O(n2+m)=O(n2)O(n^2 + m) = O(n^2) 的。

如果对于稀疏图,采用二叉堆维护边的插入情况,那么复杂度可以优化到 O((n+m)logn)O((n+m) \log n). 对于稀疏图 (m<n2/lognm < n^2/\log n), 这是一个不错的优化。

如果采用斐波那契堆优化,那么复杂度可以降低到 O(nlogn+m)O(n \log n+m).

# 多源最短路问题

# Floyd-Warshall 算法

只有三层 for , 简单快捷 *。复杂度 O(n3)O(n^3).

Floyd_Warshall
for (k = 1; k <= n; k++)
  for (x = 1; x <= n; x++)
    for (y = 1; y <= n; y++)
      f[k][x][y] = min(f[k - 1][x][y], f[k - 1][x][k] + f[k - 1][k][y]);

是否存在负权,只需要观察 f[n][i][j] 中的对角线元素 ( i==j ) 中是否存在负元即可。

# Johnson 算法(稀疏图上)

复杂度 O(n2logn+nm)O(n^2 \log n + nm).

步骤如下:

  1. 添加超级源点 ss 并初始化边。(O(n)O(n))
  2. 运行 Bellman-Ford 算法,判断是否存在负环。(O(n2+nm)O(n^2+nm))
  3. 若不存在,根据 Bellman-Ford 计算的结果进行重赋权。(O(n+m)O(n+m))
  4. 对各节点,运行 Dijkstra. (O(n(nlogn+m))=O(n2logn+mn)O(n(n \log n+m)) = O(n^2 \log n + mn)).