最短路专题

部分内容转载自:樱狸-专题·最短路 侵删

单源最短路

Dijkstra

算法介绍:

  • 算法特点:迪杰斯特拉算法使用了广度优先搜索解决赋权有向图或者无向图的单源最短路径问题,算法最终得到一个最短路径树。该算法常用于路由算法或者作为其他图算法的一个子模块。

  • 算法的思路:Dijkstra算法采用的是一种贪心的策略,声明一个数组dis来保存源点到各个顶点的最短距离和一个保存已经找到了最短路径的顶点的集合:T,初始时,原点 s 的路径权重被赋为 0 (dis[s] = 0)。若对于顶点 s 存在能直接到达的边(s,m),则把dis[m]设为w(s, m),同时把所有其他(s不能直接到达的)顶点的路径长度设为无穷大。初始时,集合T只有顶点s。
    然后,从dis数组选择最小值,则该值就是源点s到该值对应的顶点的最短路径,并且把该点加入到T中,此时完成一个顶点。
    然后,我们需要看看新加入的顶点是否可以到达其他顶点并且看看通过该顶点到达其他点的路径长度是否比源点直接到达短,如果是,那么就替换这些顶点在dis中的值。
    然后,又从dis中找出最小值,重复上述动作,直到T中包含了图的所有顶点。

void dijkstra() {
  memset(dis, 0x3f, sizeof dis);
  memset(vis, 0, sizeof vis);
  dis[1] = 0;
  for (int i = 1; i < n; i++) {
    int x = 0;//当前dis最小的点
    for (int j = 1; j <= n; j++)//vis的作用是保证每个点全局只被用来更新别的点一次。 
      if (!vis[j] && (x == 0 || dis[j] < dis[x])) x = j; 
    vis[x] = 1;
    for (int j = 1; j <= n; j++)//当然,这里用邻接表的话也可以省一些时间和空间
      dis[j] = min(dis[j], dis[x] + g[x][j]); //g是邻接矩阵 
  }
}//入口:直接调用即可。

但是看起来代码很简短,不论是时间还是空间,复杂度都很高。

所以就有了dijkstra的堆优化——不用每次O(n)地找最小的dis节点,而是用一个堆来维护,这样复杂度就可以降到O(mlogn)。

下面是堆优化的核心代码:【优化后用邻接表了】

priority_queue<pair<int, int> > q;//优先队列本为大根堆,这里的pair前者用于排序
//priority_queue<pair<int, int>, vector<pair<int, int> >, greater<pair<int, int> > > q;
//如果不想写大根堆+相反数维护的话,直接这样写小根堆也可以。
void dijkstra() {
  memset(dis, 0x3f, sizeof dis);
  dis[1] = 0;
  q.push(make_pair(0, 1));
  while (q.size()) {
    int u = q.top().second; q.pop();//取出堆顶的点
    if (vis[u]) continue;//如果已经被用来更新过别的点了,就不用了
    vis[u] = 1;
    for (int i = head[u]; i; i = e[i].next) {//这里用邻接表了,因为没有连边的点矩阵也更新不到
      int v = e[i].v;
      int w = e[i].w;
      if (dis[v] > dis[u] + w) {//可以更新
        dis[v] = dis[u] + w;
        q.push(make_pair(-dis[v], v));//dis存相反数是为了在大根堆里得到较小的那一个。
      }
    }
  }

SPFA

算法介绍:

这个算法是对Bellman-ford算法的一个队列优化。可用于求单源最短路。

如果说dijkstra是步步回头找最小,那么SPFA就是一点一圈扩散去。每到一个点,就直接枚举和它相连的所有边和点,如果走这条路可以更近,那么就更新那个点的dis。如果那个点不在即将更新的队列里,那就放进去。也就是十分类似于BFS的一种做法。在这种做法下,每个点可能被放进队列多次,但是都是带着更新前面的点的dis 的可能进去的。这样不断更新,直到已经没有哪个点更新别的点,就说明已经得到最短路了。

如果没有明白,那么可以先看看代码——

void spfa() {
  memset(dis, 0x3f, sizeof dis);
  dis[1] = 0; vis[1] = 1;//vis记录是否在队列里 
  queue<int> q;
  q.push(1);
  register int u, v;
  while(q.size()) {
    u = q.front(); q.pop(); vis[u] = 0;
    for(int i = head[u]; i; i = e[i].nxt) {//向外发散 
      v = e[i].to;
      if (dis[u] + e[i].w < dis[v]) {
        dis[v] = dis[u] + e[i].w;
	if (!vis[v]) q.push(v), vis[v] = 1; //不在队列中,那就放进去更新别的点 
      }
    }
  }
}

两种算法的核心都是找到另一条路来更新当前的最短路,但是实现方法不大一样。某种程度上说,dijkstra因为即使是堆优化,优先队列的常数也是很大的,而SPFA在稀疏图下步步发散就可以跑得很快,所以有时SPFA可以比Dijkstra快很多,算法的复杂度可以达到O(km)【k为常数】的级别。但是如果是稠密图的话,SPFA也可以退化到O(nm)。所以两种算法也是各有优势,才能一起存活下来。当然,SPFA也是可以用优先队列 优化队列的,实现方法最后就和Dijkstra差不多了。

例题

Dijkstra

传送门:P3371 【模板】单源最短路径(弱化版)

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
const int maxn = 1e7 + 10;
const int maxm = 1e4 + 10;
#define INF 1e9 + 1

int n, m, s, cnt;
int head[maxn], vis[maxn], dis[maxn];
struct edge{
  int u, v, w, next;
}e[500001];
struct node{
  int w, now;
  inline bool operator <(const node &x) const{
    return w > x.w;
  }
};
priority_queue<node>q;

inline int read() {
  int x = 0, k = 1; char ch = getchar();
  for (; !isdigit(ch); ch = getchar()) if (ch == '-') k = -1;
  for (; isdigit(ch); ch = getchar()) x = x * 10 + ch - '0';
  return x * k;
}

inline void add(int u, int v, int w) {
  e[++cnt].u = u;
  e[cnt].v = v;
  e[cnt].w = w;
  e[cnt].next = head[u];
  head[u] = cnt;
}

inline void dijkstra() {
  for (int i = 1; i <= n; i++) dis[i] = INF;
  dis[s] = 0;
  q.push((node){0, s});
  while (!q.empty()) {
    node x = q.top();
    q.pop();
    int u = x.now;
    if (vis[u]) continue;
    vis[u] = 1;
    for (int i = head[u]; i; i = e[i].next) {
      int v = e[i].v;
      if (dis[v] > dis[u] + e[i].w) {
  	dis[v] = dis[u] + e[i].w;
        q.push((node){dis[v], v});
      }
    }
  }
}

signed main() {
  n = read(); m = read(); s = read();
  for (int i = 1; i <= m; i++) {
    register int u, v, w;
    u = read(); v = read(); w = read();
    add(u, v, w);
  }
  dijkstra();
  for (int i = 1; i <= n; i++) {
    if (dis[i] != INF) cout << dis[i] << " "; 	
    else cout << 2147483647 << " ";
  }
  return 0;
}

P4779 【模板】单源最短路径(标准版)

#include <iostream>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <set>
#include <map>
#include <vector>
#include <queue>
#define ll long long
#define INF 0x7fffffff
using namespace std;
const int maxn = 1e5 + 10;
const int maxm = 5e5 + 10;

struct node{
  int to, nxt, dis;
}e[maxm];
int head[maxn], dis[maxn], vis[maxn];
int n, m, s, cnt;
priority_queue< pair<int, int> >q;

inline ll read() {
  ll x = 0, k = 1;
  char ch = getchar();
  for (; !isdigit(ch); ch = getchar()) if (ch == '-') k = -1;
  for (; isdigit(ch); ch = getchar()) x = x * 10 + ch - '0';
  return x * k;
}

inline void add(int u, int v, int num) {
  e[++cnt].to = v;
  e[cnt].nxt = head[u];
  e[cnt].dis = num;
  head[u] = cnt;
}

inline void dijkstra() {
  dis[s] = 0;
  q.push(make_pair(0, 1));
  while (q.size()) {
    int u = q.top().second; 
    q.pop();
    if (vis[u]) continue;
    vis[u] = 1;
    for (int i = head[u]; i; i = e[i].nxt) {
      int v = e[i].to;
      int w = e[i].dis;
      if (dis[v] > dis[u] + w) {
      	dis[v] = dis[u] + w;
      	q.push(make_pair(-dis[v], v));
      }
    }
  }
}

inline void init() {
  for (int i = 1; i <= n; i++) dis[i] = INF;
}

int main() {
  ios::sync_with_stdio(false);
  n = read(); m = read(); s = read();
  init();
  for (int i = 1; i <= m; i++) {
    int u, v, d;
    u = read(); v = read(); d = read();
    add(u, v, d);
  }
  dijkstra();
  for (int i = 1; i <= n; i++) printf("%d ", dis[i]);
  return 0;
}

SPFA

P1144 最短路计数


#include <iostream>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <set>
#include <map>
#include <vector>
#include <queue>
#define ll long long
#define INF 0x7fffffff
#define mod 100003
using namespace std;
const int maxn = 1e6 + 10;
const int maxm = 2e6 + 10;

struct node{
  int to, nxt, dis;
}e[maxm];
int head[maxn], dis[maxn], vis[maxn], num[maxn];
int n, m, cnt;

inline ll read() {
  ll x = 0, k = 1; char ch = getchar();
  for (; !isdigit(ch); ch = getchar()) if (ch == '-') k = -1;
  for (; isdigit(ch); ch = getchar()) x = x * 10 + ch - '0';
  return x * k;
}

inline void init() {
  for (int i = 1; i <= n; i++) dis[i] = INF;
}

inline void add(int u, int v, int num) {
  e[++cnt].to = v;
  e[cnt].nxt = head[u];
  e[cnt].dis = num;
  head[u] = cnt;
}

inline void spfa() {
  //memset(dis, INF, sizeof(dis));
  dis[1] = 0; vis[1] = 1; num[1] = 1;
  queue<int> q;
  q.push(1);
  register int u, v;
  while (q.size()) {
    u = q.front(); q.pop(); vis[u] = 0;
    for (int i = head[u]; i; i = e[i].nxt) {
      v = e[i].to;
      if (dis[u] + e[i].dis < dis[v]) {
      	num[v] = num[u] % mod;
      	dis[v] = dis[u] + e[i].dis;
      	if (!vis[v]) q.push(v), vis[v] = 1;
      }
      else if (dis[u] + e[i].dis == dis[v]) 
        num[v] = (num[v] + num[u]) % mod;
    }
  }
}

int main() {
  n = read(); m = read();
  init(); 
  for (int i = 1; i <= m; i++) {
  	register int x, y;
    x = read(); y = read();
    add(x, y, 1);
    add(y, x, 1);
  }
  spfa();
  for (int i = 1; i <= n; i++) printf("%d\n", num[i]);
  return 0;
}

多源最短路

Floyd

原理十分的简单粗暴——直接看代码都能明白。【读入用的邻接矩阵,初始化极大值,直接读入边与边的关系存入dis】

void Floyd() {
  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]);
}

顺带解释一下为什么k的这层循环要放在最外面。因为如你所见,Floyd的本质其实是dp。dp需要的是什么?阶段和状态。如果说到了点(i,j)这就是状态的话,那么阶段呢?就是K了。因为Floyd的原始写法其实是:dp[k][i][j] = min(dp[k-1][i][j], dp[k-1][i][k] + dp[k-1][k][j]),其中k表示的是用第1~k号以内的点,得到的i到j的最短路。所以k其实也是表示的阶段,而阶段的枚举是一定要放在最外层的。

  • 传递闭包

Floyd算法不仅可以实值求最短路,也可以维护关系——比如,当前值能不能通过已经更新出来了的东西 更新出来。具有传递性。

同理,建立邻接矩阵,d[i][j] = 1 表示 i 和 j 有关系,为0表示没有关系。

可以用Floyd求出所有的关系:

void Floyd() {
  for (int k = 1; k <= n; k++)//注意,一定要先枚举中转节点保证三角形的情况
    for(int i = 1; i <= n; i++)
      for(int j = 1; j <= n; j++)
	dis[i][j] |= dis[i][k] & dis[k][j];
}

看起来可能确实没什么用,但是当你在求某个问题过后,你需要得到在当前环境下某两个点是否联通,就可以直接Floyd传递闭包走一波,然后看dis[i][j] & dis[j][i]是否为1即可。

例题

P1522 [USACO2.4]牛的旅行 Cow Tours

#include <iostream>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <set>
#include <map>
#include <vector>
#include <queue>
#define ll long long
#define INF 0x7fffffff
#define mod 100003
using namespace std;
const int maxn = 1e4 + 10;

int n, xx;
struct node{
  int x, y;
}a[maxn];
double dis[maxn][maxn], num[maxn];
double ans1, ans2 = INF, ans;

inline ll read() {
  ll x = 0, k = 1; char ch = getchar();
  for (; !isdigit(ch); ch = getchar()) if (ch == '-') k = -1;
  for (; isdigit(ch); ch = getchar()) x = x * 10 + ch - '0';
  return x * k;
}

inline double cal(int i, int j) {
  return sqrt((a[i].x - a[j].x) * (a[i].x - a[j].x) + (a[i].y - a[j].y) * (a[i].y - a[j].y));
}

int main() {
  n = read();
  for (int i = 1; i <= n; i++) a[i].x = read(), a[i].y = read();
  for (int i = 1; i <= n; i++)
    for (int j = 1; j <= n; j++) {
      scanf("%1d", &xx);
      if (xx) dis[i][j] = cal(i, j);
      else if (i != j) dis[i][j] = INF;
    }
  for (int k = 1; k <= n; k++)
    for (int i = 1; i <= n; i++)
      for (int j = 1; j <= n; j++) 
        if (dis[i][k] + dis[k][j] < dis[i][j])
  	  dis[i][j] = dis[i][k] + dis[k][j];
  for (int i = 1; i <= n; i++)
    for (int j = 1; j <= n; j++) {
      if (dis[i][j] != INF) num[i] = max(num[i], dis[i][j]);
      ans1 = max(ans1, num[i]);
    }
  for (int i = 1; i <= n; i++)
    for (int j = 1; j <= n; j++)
      if (dis[i][j] == INF)
  	ans2 = min(num[i] + cal(i, j) + num[j], ans2);
  ans = max(ans1, ans2);
  printf("%.6lf", ans);
  return 0;
}

其余的有缘再更新~

posted @ 2021-03-02 15:00  Moominn  阅读(98)  评论(0编辑  收藏  举报