图论——最短路

这是图论中的常见问题,有三种常用算法,以及许多拓展内容。

松弛操作

对于每条有向边 ( u , v , w ) (u,v,w) (u,v,w),令 d i s ( v ) = m i n ( d i s ( v ) , d i s ( u ) + w ) dis(v) = min(dis(v), dis(u) + w) dis(v)=min(dis(v),dis(u)+w)。这是最短路算法的核心操作。

Dijkstra

适用于非负权图求解单源最短路径。

流程

1、初始化 d i s ( dis( dis(源点 ) = 0 ) = 0 )=0,其余结点 d i s dis dis I N F INF INF
2、找到一个未被标记的, d i s ( u ) dis(u) dis(u)最小的结点 u u u,然后标记 u u u
3、扫描 u u u的出边 ( u , v , w ) (u,v,w) (u,v,w),进行松弛。
4、重复上述步骤,直到所有结点都被标记。

正确性证明

设当前被选中的结点为 u u u,当所有边权非负时, d i s ( u ) dis(u) dis(u)不可能被其他结点更新,故每次选出的结点 u u u一定满足 d i s ( u ) dis(u) dis(u)为从源点到 u u u的最短路径。

void Dijkstra(int s) {
  memset(dis, 0x3f, sizeof(dis));
  memset(vis, false, sizeof(vis));
  dis[s] = 0;
  for (int j = 1, u; j < n; ++j) {
    u = 0;
    for (int i = 1; i <= n; ++i)
      if (!vis[i] && dis[i] < dis[u]) u = i;
    vis[u] = true;
    for (int i = G[u], v, w; i != 0; i = e[i].nxt) {
      v = e[i].v, w = e[i].w;
      dis[v] = min(dis[v], dis[u] + w);
    }
  }
}

上面这段代码的时间复杂度为 O ( n 2 ) O(n^2) O(n2),瓶颈在于查询全局最小值。可以使用线段树,堆,平衡树等数据结构优化复杂度。这里给出堆优化的代码。

void Dijkstra(int s) {
  // first 为 dis 值,second 为点的编号
  static priority_queue<pair<int, int>,
    vector<pair<int, int>, greater<pair<int, int> > > Q;
  memset(dis, 0x3f, sizeof(dis));
  memset(vis, false, sizeof(vis));
  dis[s] = 0;
  Q.push(make_pair(0, s));
  while (!Q.empty()) {
    int u = Q.front().second; Q.pop();
    if (vis[u]) continue;
    vis[u] = true;
    for (int i = G[u]; i != 0; i = e[i].nxt) {
      v = e[i].v, w = e[i].w;
      dis[v] = min(dis[v], dis[u] + w);
      Q.push(make_pair(dis[v], v));
    }
  }
}

堆优化Dijkstra的时间复杂度为 O ( m log ⁡ n ) O(m\log n) O(mlogn)

题目

Bellman Ford & SPFA

适用于求解单源最短路径。

思路

对于一张图,若每条边 ( u , v , w ) (u,v,w) (u,v,w)都满足 d i s ( v ) < = d i s ( u ) + w dis(v)<=dis(u)+w dis(v)<=dis(u)+w,则此时的 d i s dis dis就是所求最短路。

流程

1、扫描每条边,进行松弛。
2、重复上步骤,知道没有松弛操作为止。
以上便是Bellman Ford算法
时间复杂度 O ( n m ) O(nm) O(nm)

优化

考虑到上面的过程中,每个点的 d i s dis dis即使没有被更新,也会扫描其出边。于是可以用队列保存要更新的结点,避免无效的扫描。

void SPFA() {
  static queue<int> Q;
  memset(dis, 0x3f, sizeof(dis));
  memset(inQ, false, sizeof(inQ));
  dis[s] = 0;
  Q.push(s), inQ[s] = true;
  while (!Q.empty()) {
    int u = Q.front();
    Q.pop(), inQ[u] = false;
    for (int i = G[u], v, w; i != 0; i = e[i].nxt) {
      v = e[i].v, w = e[i].w;
      if (dis[v] > dis[u] + w) {
        dis[v] = dis[u] + w;
        if (!inQ[v]) Q.push(v), inQ[v] = true;
      }
    }
  }
}

时间复杂度还是 O ( n m ) O(nm) O(nm)。。。
但是在随机数据的稀疏图下效率极高,接近线性。
不过可以被特殊数据卡掉,所以非负权图还是尽量用Dijkstra。

Floyd

用于求解多源最短路径。

思路

Floyd 基于 DP,用 d i s ( k , i , j ) dis(k,i,j) dis(k,i,j)表示经过若干个编号不超过 k k k的结点时从 i i i j j j的最短路径。于是可以写出转移
d i s ( k , i , j ) = m i n ( d i s ( k − 1 , i , j ) , d i s ( k − 1 , i , k ) + d i s ( k − 1 , k , j ) ) dis(k,i,j)=min(dis(k-1,i,j),dis(k-1,i,k)+dis(k-1,k,j)) dis(k,i,j)=min(dis(k1,i,j),dis(k1,i,k)+dis(k1,k,j))
与背包问题类似, k k k这一维度实际上进行了一个总 d i s ( k − 1 ) dis(k-1) dis(k1) d i s ( k ) dis(k) dis(k)的拷贝,所以可以用滚动数组优化空间。

for (int k = 1; k <= n; ++k)
  for (int i = 1; i <= n; ++i)
    for (int j = 1; j <= n; ++j)
      dis[i][j] = min(dis[i][j], dis[i][k] + dis[k][j]);

时间复杂度 O ( n 3 ) O(n^3) O(n3),在稠密图上效率较高。

稀疏图的最短路可以转化为 n n n个结点的单源最短路径求解。

拓展

动态加边、加点

当在原图中加入一条边或一个点后,最短路可能会更新。如果每加一条边或一个点都重新跑一次 Floyd,时间复杂度可能难以接受。
考虑到 Floyd 基于 DP 实现,所以在更新时没有必要全局更新,只需更新所加的边或点可能影响的 d i s ( i , j ) dis(i,j) dis(i,j)
加边

// (u,v,w)
for (int i = 1; i <= n; ++i)
  for (int j = 1; j <= n; ++j)
    dis[i][j] = min(dis[i][j], dis[i][u] + w + dis[v][j]);

加点

// k
for (int i = 1; i <= n; ++i)
  for (int j = 1; j <= n; ++j)
    dis[i][j] = min(dis[i][j], dis[i][k] + dis[k][j]);

负环

定理: 当一张图中存在负环时,最短路无解。
判定负环可以用SPFA
l e n ( u ) len(u) len(u)表示一个点的最短路上经过了几个点。显然当 l e n ( u ) > = n len(u)>=n len(u)>=n时,原图存在负环。

bool SPFA() {
  // 存在负环时返回 false
  static queue<int> Q;
  memset(dis, 0x3f, sizeof(dis));
  memset(inQ, false, sizeof(inQ));
  memset(len, 0, sizeof(len));
  dis[s] = 0, len[s] = 1;
  Q.push(s), inQ[s] = true;
  while (!Q.empty()) {
    int u = Q.front();
    Q.pop(), inQ[u] = false;
    for (int i = G[u], v, w; i != 0; i = e[i].nxt) {
      v = e[i].v, w = e[i].w;
      if (dis[v] > dis[u] + w) {
        dis[v] = dis[u] + w;
        if (!inQ[v]) {
          len[v] = len[u] + 1;
          if (len[v] >= n) return false;
          Q.push(v), inQ[v] = true;
        }
      }
    }
  }
  return true;
}

最小环

无向图

先跑一遍Floyd,求出最短路。
然后求最小的 d i s ( i , j ) + d i s ( j , k ) + d i s ( k , i ) dis(i,j)+dis(j,k)+dis(k,i) dis(i,j)+dis(j,k)+dis(k,i)

有向图

枚举每个 s s s为源点,将所有 ( u , v ) (u,v) (u,v)中的 v v v d i s ( v ) = w ( u , v ) dis(v)=w(u,v) dis(v)=w(u,v)加入堆中。然后跑一遍Dijkstra,得出的 d i s ( s ) dis(s) dis(s)就是通过 s s s的最小环长度。

分层图

图论中一种重要的模型。

邻接矩阵乘法

对于一张图,设 G G G为其邻接矩阵,则 G k G^k Gk中, G k ( u , v ) G^k(u,v) Gk(u,v)表示从 u u u v v v路径长度为 k k k的路径条数。

你可能感兴趣的:(图论——最短路)