最短路算法笔记

本文详细介绍了图的四种经典最短路径算法:Dijkstra的朴素实现和堆优化、Bellman-Ford算法以及SPFA算法,包括它们的核心思想、代码实现和适用场景。此外,还提到了Floyd算法用于所有点对间最短路径的计算。文章特别强调了负权边和负环对这些算法的影响,并提供了检测负环的方法。
摘要由CSDN通过智能技术生成

原图来自于wmy0217_大佬的文章,本文很多部分都参考了他的文章,特此注明。
img

01 存储

稀疏图:n和m的数量级相等,用邻接矩阵来存储;

稠密图:n 2 和m的数量级相同,用邻接表存储;

02 Dijkstra(朴素版)

核心思想:两层循环,第一层循环找离源点最近的点,第二层循环更新所有点到源点的距离;

核心代码:

	for (int i = 1; i <= n; ++i)
	{
		int t = -1;
		for (int j = 1; j <= n; ++j)
		{
			if (!st[j] && (t==-1 || dist[j] < dist[t]))	
				t = j;
		}
		st[t] = true;	// 已找到 
		
		// 更新距离 
		for (int j = 1; j <= n; ++j)
		{
			dist[j] = min(dist[j], dist[t] + g[t][j]); 
		}
	}

注意事项:不能处理负环,并且g和dist都要初始化为正无穷(用0x3f来代替)。

样板题

给定一个n个点m条边的有向图,图中可能存在重边和自环,所有边权均为正值。

请你求出1号点到n号点的最短距离,如果无法从1号点走到n号点,则输出-1。

输入格式
第一行包含整数n和m。

接下来m行每行包含三个整数x,y,z,表示存在一条从点x到点y的有向边,边长为z。

输出格式
输出一个整数,表示1号点到n号点的最短距离。

如果路径不存在,则输出-1。

数据范围
1≤n≤500,
1≤m≤105,
图中涉及边长均不超过10000。

输入样例:
3 3
1 2 2
2 3 1
1 3 4

输出样例:
3

参考代码

#include <cstdio>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 510;
int g[N][N];
bool st[N];
int dist[N];

int n, m;


int main()
{
	memset(g, 0x3f, sizeof g);
	memset(dist, 0x3f, sizeof dist);
	
	scanf("%d%d", &n, &m);
	
	for (int i = 0; i < m; ++i)
	{
		int x, y, z;
		scanf("%d%d%d", &x, &y, &z);
		g[x][y] = min(z, g[x][y]);
	}
	
	dist[1] = 0;
	
	for (int i = 1; i <= n; ++i)
	{
		int t = -1;
		for (int j = 1; j <= n; ++j)
		{
			if (!st[j] && (t==-1 || dist[j] < dist[t]))	
				t = j;
		}
		st[t] = true;	// 已找到 
		
		// 更新距离 
		for (int j = 1; j <= n; ++j)
		{
			dist[j] = min(dist[j], dist[t] + g[t][j]); 
		}
	}
	
	if (dist[n] == 0x3f3f3f) printf("-1\n");
	else printf("%d", dist[n]);
	
	return 0;
 } 

03 Dijkstra(堆优化)

适用于稀疏图,用到了邻接表,个人使用了数组表示邻接表的方法,整理在了附录,可以先去看看。

优化方法:用堆排序,每次取出一个dis值最小的pair,second为点,first为第一个点的距离,也就是前面的求取最近点的方法,然后根据该点更新所有的点的距离,直到所有点遍历完(heap为空)。

和朴素的题目一样,但稍微修改一下数据,例如把N修改成1e5,这样朴素版必定超时(1e5 * 1e5 = 1e10);

而堆优化版本的时间复杂度为O(mlogn),则不会超时。

示例代码如下:

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <queue>

using namespace std;

const int N = 2e5+10;
int h[N], ne[N], e[N], w[N], idx;
bool st[N];
int dist[N];
typedef pair<int, int> PII;

int n, m;

void add(int a, int b, int c)
{
	e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++;
}

int dijkstra()
{
	memset(dist, 0x3f, sizeof dist);
	dist[1] = 0;
	
	priority_queue<PII, vector<PII>, greater<PII> > heap;
	heap.push({0, 1}); 		// first是距离,second是点
	
	while(heap.size())
	{
		PII t = heap.top();
		heap.pop();
		
		int dis = t.first , ver = t.second;
		if (st[ver]) continue;				// 因为堆优化,弹出来的一定是dis最小的,所以重边只看第一个,后面直接跳过就行了 
		st[ver] = true;
		
		// 根据最短边更新距离
		for (int i = h[ver]; i != -1; i = ne[i])
		{
			int j = e[i];
			if (dis+w[i] < dist[j])
			{
				dist[j] = dis + w[i];	
				heap.push({dist[j], j});
			}
		}		
	} 
	if (dist[n] == 0x3f3f3f3f) return -1;
	else return dist[n];
}

int main()
{	
	memset(h, -1, sizeof h);	// 需要在add()之前初始化,否则会覆盖h的值 
	cin >> n >> m;
	while(m--)
	{
		int a, b, c;
		scanf("%d%d%d", &a, &b, &c);
		add(a, b, c);
	}

	
	cout << "shortest distance of 1 to n: " << dijkstra() <<  endl; 

	return 0;
 } 

运行结果如图:

image-20230507152428470

04 Bellman-ford算法

主要算法:循环N次,每次循环遍历每条边;

主要代码:

for(int i=0; i<n; i++)
  for(int j=0; j<m; j++)
  {
  	 if(dist[a]+w<dist[b])
  	   dist[b] = dist[a] + w; //w是a->b的权重 
  }

例题:853. 有边数限制的最短路

参考代码

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 510;
const int M = 10010;

struct Edge{
    int a, b, c;  
}edge[M];

int dist[N], backup[N];
bool st[N];
int n, m, k;

void bellman_ford()
{
    for (int i = 0; i < k; ++i)
    {
        memcpy(backup, dist, sizeof backup);
        for (int j = 0; j < m; ++j)
        {
            int a = edge[j].a, b = edge[j].b, c = edge[j].c;
            dist[b] = min(dist[b], backup[a]+c);
        }
    }
    // 处理负权边
    if (dist[n] > 0x3f3f3f3f/2) cout << "impossible" << endl;
    else cout << dist[n] << endl;
}

int main()
{
    memset(dist, 0x3f, sizeof dist);
    cin >> n >> m >> k;
    for (int i = 0; i < m; ++i)
    {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        edge[i] = {a, b, c};
    }
    dist[1] = 0;
    
    bellman_ford();
}

05 SPFA算法

优化后的Bellman-ford算法,优化思路:只有更新后的点,更新边才会更小,所以没更新的点直接跳过;

核心代码:

int spfa()
{// bool st[N]: 存第 i 个点是不是在队列中,防止存重复的点 
	memset(dist,0x3f,sizeof(dist));
	dist[1] = 0;
	
	queue<int> q; //存储所有待更新的点
	q.push(1);  // 1号点入队 
	st[1] = true;
	while(q.size()) // 队列不空
	{
	   int t = q.front(); //取队头 
	   q.pop();
	   st[t] = false; // 代表这个点已经不在队列了,因为存在边权为负数,某个点可能会被更新多次,所以可以多次入队和出队。
	   
	   for(int i = h[t]; i!=-1; i=ne[i]) // 更新 t 的所有临边结点的最短路 
	   {
	   	 int j = e[i];
	   	 if(dist[j] > dist[t]+w[i])
	   	 {
	   	    dist[j] = dist[t] + w[i];
			if(!st[j])  //如果 j 不在队列,让 j 入队 
			{
				q.push(j); 
				st[j] = true;  // 标记 j 在队中 
			} 	    	
		 }
	   }	
	} 
	 if(dist[n] == 0x3f3f3f3f) return -1; // 不存在最短路 
	 return dist[n]; 
}

例题洛谷P3371

参考代码:

#include <iostream>
#include <cstdio>
#include <queue>
#include <cstring>

using namespace std;

const int N = 1e4 + 10;
const int M = 5e5 + 10;
int h[N], e[M], ne[M], w[M], idx;
bool st[N];
int dist[N];
int n, m, s;

void add(int a, int b, int c)
{
	e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++;
}

void spfa(int s)
{
	memset(dist, 0x3f, sizeof dist);
	queue<int> q;
	q.push(s);
	st[s] = true;
	dist[s] = 0;
	
	while (q.size())
	{
		int t = q.front();	// 与dijsktra的区别之一,普通queue没有top() 
		q.pop();
		st[t] = false;
		
		for (int i = h[t]; i != -1 ; i = ne[i])
		{
			int j = e[i];
			
			if (dist[j] > dist[t] + w[i])
			{
				dist[j] = dist[t] + w[i];
				if (!st[j])
				{
					q.push(j);
					st[j] = true;
				}
				
			}
		}
	}
}

int main()
{
	memset(h, -1, sizeof h);
	cin >> n >> m >> s;
	for (int i = 0; i < m; ++i)
	{
		int a, b, c;
		scanf("%d%d%d", &a, &b, &c);
		add(a, b, c);
	}
	
	spfa(s);
	
	for (int i = 1; i <= n; ++i)
	{
		if (dist[i] == 0x3f3f3f3f) cout << 2147483647 << " ";
		else cout << dist[i] << " ";
	}
	
	return 0;
}

带有负环的图会导致最短路径失败,因为无限次经过负环可以使路径长度无限减小。但可以使用 SPFA 算法检测负环,方法是记录一个点被加入队列的次数,如果有一个点被加入队列的次数超过了节点数量,则说明存在负环。

因此,使用 SPFA 算法处理带负环的最短路径问题时,通常需要在算法外围添加一个负环检测的过程,如果检测到了负环,则认为不存在最短路径。如果没有检测到负环,则可以利用 SPFA 算法计算最短路径。

以下是带负环检测的,核心是一个 cnt 数组,如果 cnt>=n 了,因为1—》x的最多经历n个点,就认为存在负环。

int spfa()
{
	queue<int> q; 
    for(int i=1; i<=n; i++) //将所有结点入队
	{
	    st[i] = true;
		q.push(i);	  
    }
	while(q.size()) // 队列不空
	{
	   int t = q.front(); //取队头 
	   q.pop();
	   st[t] = false; // 代表这个点已经不在队列了
	   
	   for(int i = h[t]; i!=-1; i=ne[i]) // 更新 t 的所有临边结点的最短路 
	   {
	   	 int j = e[i];
	   	 if(dist[j] > dist[t]+w[i])
	   	 {
	   	    dist[j] = dist[t] + w[i];
	   	    cnt[j] = cnt[t] + 1; // t到起点的边数+1 
	   	    
	   	    if(cnt[j] >= n) return true;// 存在负环 
			if(!st[j])  //如果 j 不在队列,让 j 入队 
			{
				q.push(j); 
				st[j] = true;  // 标记 j 在队中 
			} 	    	
		 }
	   }	
	} 
	 return false;// 不存在负环 
}

06 Floyd算法

基于动态规划,

  1. 直接 i 到 j;
  2. i 经过若干个结点到 k 再到 j
  3. 对于每一个k,我们都判断 d[i] [j] 是否大于 d[i] [k] + d[k] [j],如果大于,就可以更新d[i] [j]了;
void floyd()
{
 for(int k=1; k<=n; k++)
   for(int i=1; i<=n; i++)
     for(int j=1; j<=n; j++)
      d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
}

附录 数组表示邻接表

01 需要的变量

方便使用,直接设为全局变量;

const int N = 1e3+10;	// 点
const int M = 2e6+10;	// 边
int h[N];				// h[i]表示以点i为头节点的边的编号
int e[2*M];				// e[i]表示第i条边对应的终点
int ne[2*M];			// ne[i]表示第i条边的下一条边的序号
int idx;				// 边的序号,从0开始
02 初始化数组

方便遍历,需要中止条件,所以全部初始化为1,当遍历到-1时退出;

memset(h, -1, sizeof h);
03 添加边
void add(int a, int b) 	// 添加一条起点为a,终点为b的边
{
    e[idx] = b;			// idx边的终点设为b
    ne[idx] = h[a];		// 下一条边的序号为h[a],如果为-1,表示没有下一条边
    h[a] = idx++;		// 这条边的序号设为idx
}

纠错:下图中 e[1] 其实是 e[0],写的时候没注意!

image-20230507120320212

如图所示,如果没有其他边,就会指向-1,代表没有下一条边;

如果有,就会将当前边设为第一条,指向之前已经存在的边(c-a)。

04 邻接表的遍历

把每个节点当作头节点,然后遍历其中的存储的邻接点,直到遍历到-1为止;

for (int k = 0; k < N; ++k)
{
    if (h[k] != -1)
    {
        cout << "表头节点:" << h[k] << " 邻接点:";
		for (int i = h[k]; i != -1; i = ne[i])	// i代表边
		{
			cout << e[i] << " ";	// 代表节点(i边的终点)
		}        
        cout << endl;
    }
}
05 图的遍历
void dfs(int u)
{
    st[u] = true;
    cout << u << " ";
    for (int i = h[u]; i != -1; i = ne[i])
    {
       	int j = e[i];
        if (j != -1)
        {
            dfs(j);
		}
	}
}
06 完整demo
#include <iostream>
#include <cstdio>
#include <cstring>

using namespace std;

const int N = 1e3 + 10;	
const int M = 2e6 + 10;
int h[N], e[2 * M], ne[2 * M], idx;
bool st[N];

void add(int a, int b)
{
	e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}

void travel()
{
	for (int k = 0; k < N; ++k)
	{
		if (h[k] != -1) 
		{
			cout << "h: " << h[k] << " other: ";	// 边的节点 
			for (int i = h[k]; i != -1; i = ne[i])
			{
				int j  = e[i];
				cout << j << " ";
			}
			cout << endl;
		}
	}
}

void dfs(int u)
{
	st[u] = true;
	cout << u << " ";
	
	for (int i = h[u]; i != -1; i = ne[i])
	{
		int j = e[i];
		if (!st[j])	// 防止出现环 
		{
			dfs(j);
		}
	}
}


int main()
{
	memset(h, -1, sizeof h);
	add(1, 3);
	add(2, 4);
	add(1, 2);
	add(3, 5);
	
	travel();
	cout << "dfs graph" << endl;
	dfs(1);
	
	return 0;
}

运行结果如图:

image-20230507133154611

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值