单源最短路

  • 就是从一个起点到终点的问题

边权为正数:

朴素Dijkstra算法(贪心)

  • 时间复杂度是
  • 但是时间复杂度与边数无关,与问题规模有关,适用于解决 稠密图
  • 首先有起点,每次找到一个距离起点最近的点,找到之后记录找过,再用这个点到其他点的距离来更新起点到其他点的距离,再找一个次近的点(没记录过),再更新,一直找到最后一个点(n)
  • 找n个点,每个点更新之后点的距离,两个循环
    • 稠密图要用邻接矩阵来存

具体实现

  • 开一个邻接矩阵,存下边数和权重,可全初始化为无穷(0x3f3f3f3f)
  • 开一个数组存起点到各个点的距离
  • 每次查找 没有找过的, 距离起点最近的点,用其权重(到其他点的距离)来更新起点到每个点的距离
  • 距离初始也是赋为无穷
int Dijkstra()
{
  memset(d, 0x3f, sizeof d);
  d[1] = 0;  // 起点
 
  for(int i = 0; i < n; i ++)
  {
	  int t = -1;
	  for(int j = 1; j <= n; j ++)
	  {
		  if(!st[j] && (t == -1 || d[t] > d[j]))
			  t = j;  // 找到还没有找过的距离最近的点
	  }
 
	  st[t] = true;  // 表示找过了,说明该点的最短路已经确定了
 
	  for(int j = 1; j <= n; j ++)
	  {
		  d[j] = min(d[j], d[t] + g[t][j]);  //用找到的最近的点来更新起点到每一个点的距离
	  }
  }
 
  if(d[n] == 0x3f3f3f3f) return -1;
  else return d[n];
}

堆优化Dijkstra算法

  • 时间复杂度是

  • 效率与边数有关,一般效率高,但是当m(边数)太大,就不应该使用,改用朴素的Dijkstra算法,适用于 稀疏图

  • 用小根堆来存,每次取根节点(距离最近的点)来更新

  • 同时使用一个bool数组来判断是否读取过,因为小根堆实现使用 优先队列 ,有可能存入多次同一元素的更新结果(判断只是为了不重复更新,记录多次同一元素,先出来的也是最小的,后面再找一次不会有任何实际操作,所以判断后退出)

int Dijkstra()
{
 priority_queue<PII, vector<int>, greater<PII>> heap;
 memset(d, 0x3f, sizeof d);
 d[1] = 0;
 
 heap.push({d[1], 1});
 
 while(heap.size())
 {
     auto t = heap.top();  //取最小
     heap.pop();
 
     int ver = t.second, dist = t.first;
 
     if(st[ver]) continue;  // 比如第一次边权重为3 第二次重边的权重是2, 第一次三会在遍历时进到堆里,第二次2也会满足条件进到堆里,但是结果是2的先出
     else st[ver] = true;  // 没找过的 找到后标记找过
 
     for(int i = h[ver]; i != -1; i = ne[i])
     {
   	  int j = e[i];
   	  if(d[j] > dist + w[i])
   	  {
   		  d[j] = dist + w[i];
   		  heap.push({d[j], j});  // 初始d无穷,全部都进,之后进队要求有更新
   	  }
     }
 }
 
 if(d[n] == 0x3f3f3f3f)  return -1;
 return d[n];
 
}

边权有负数:

Bellman-Ford算法(DP)

  • 时间复杂度是

  • n 次 m 条边

  • 能解决经过边数有所限制的问题

  • 松弛操作

    • 对比 每个点到起点的距离 和 经过当前最近点 再加上 最近点到该点的距离
    • 判断哪个近,做更新(和Dijkstra一样
  • 三角不等式

  • 负环

    • 有负环则做不了最短路(在要找的路径上)
    • 可以有 循环 n 次, 即找了n条边,如果在n个结点中找一个点的最短路时找了n条边,即起点到该点的最短路出现了大于 n - 1 的数,就证明路径上出现了负环(bellman -ford算法每次找最短距离,此时能找出多于 n - 1的边数,证明在找时经过最小路径的重复,重复后还能是最短路径,证明回路总权重是负值)
  • 迭代的含义

    • 第一个循环决定了迭代次数,迭代n次就是找到不超过k条边的最短路径
int bellman_ford()
{
memset(d, 0x3f, sizeof d);
	d[1] = 0;
	
	for(int i = 0; i < k; i ++)
  {
		memcpy(backup, d, sizeof d);  //复制上一次的d给backup数组
		for(int j = 0; j < m; j ++)  // 每次遍历所有边
	  {
			int a = edges[j].a, b = edges[j].b,  w = edges[j].w;  // 用结构体存每条边
			d[b] = min(d[b], backup[a] + w);  // 更新距离
			//用的是上一次的d,这样保证了找的是经过小于等于 i 的边的最短距离
			// 否则会更新为Dijkstra一样的最短距离,如第一次更新 1-3距离更新为 1-2-3 距离,这样超出了i条边
	  }
  }
	return d[n];
}
 
if(d[n] > 0x3f3f3f3f / 2)  false;  // 意思是,n结点的数可能被改了,但是按题目范围不能改太多
									   //所以用这个判断

SPFA算法

  • 时间复杂度一般是 , 最坏是

  • 无法解决经过边数有限的问题

  • 其实可以用来写 Dijkstra算法的题,而且更快,但是当 nm 过大时,不用

  • 使用邻接表

  • 其实是对bellman算法的优化,判断结点d[a]是否有更新,有才更新其所连接的结点

  • 使用队列存储更新的结点,在一次更新中重复存储一个结点没有意义,其距离会在更新中选择最短的距离,所以存多次的每次更新都一样,所以不用存

#include <queue>
 
void spfa()
{
	memset(d, 0x3f, sizeof d);
	d[1] = 0;
	q.push(1);
	st[1] = true;
	
	while(q.size())
   {
		auto t = q.front();
		q.pop();
		st[t] = false;
		for(int i = h[t]; i != -1; i = ne[i])
	  {
			int j = e[i];
			if(d[j] > d[t] + w[i])  // 是否更新
		  {
			  d[j] = d[t] + w[i];
			  if(!st[j])  // 判断是否加进队了,如果加进去了就不用再加一次,但是其距离的更新还是要更新的
			  {
				  q.push(j);
				  st[j] = true;
			  }
		  }
	}
}
 
	if(d[n] > 0x3f3f3f3f / 2)  cout << "NO" << endl;
	else cout << d[n] << endl;
}

多源汇最短路

很多起点到多终点的最短路

Floyd算法

  • 时间复杂度
  • 暴力三重运算,基于动态规划
    • 找 i 到 j 的最短距离,从 i 出发,找所有点,看 i 到 k 的距离 + k 到 j 的距离 是否会小于 i 经过 小于等于 k - 1 个点,到 j 的距离
    • k的循环相当于找经过k个点的结果
[[include]] <iostream>
[[include]] <cmath>
 
int d[N][N];  //邻接矩阵,先存下所有边
 
//邻接阵初始化
for(int i = 1; i <= n; i ++)
	for(int j = 1; j <= n; j ++)
		if(i == j)  d[i][j] = 0;
		else  d[i][j] = INF;  //无穷
 
void floyd()
{
	for(int k = 1; k <= n; k ++)  //先用k点来看所有经过k点的点
		for(int i = 1; i <= n; i ++)  //i点 经过 k 点到达目的点
		  for(int j = 1; j <= n; j ++) // 经i点的点到达所有点的距离更新
			d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
}