单源最短路的相关算法模板

一   Dijkstra 算法(只适用于权重为正值的题目

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

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

输入格式

第一行包含整数 n 和 m。

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

输出格式

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

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

1   朴素版的Dijkstra 算法

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 510;
int n, m;// n为节点个数,m为总边数
int g[N][N];//稠密图 邻接矩阵存储
int dist[N];//储存各点到起点的距离
bool st[N];
//用于在更新最短距离时 判断当前的点的最短距离是否确定 是否需要更新
//用一个状态数组 state 记录是否找到了源点到该节点的最短距离。
//state[i] 如果为真,则表示找到了源点到节点 i 的最短距离,state[i] 
//如果为假,则表示源点到节点 i 的最短距离还没有找到。
int dijkstr()
{
    //初始化距离为正无穷
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;//一号点到起点的距离为0;
    //迭代n次
    for (int i = 1; i <= n; i++)//因为每次循环中都可以确定一个最短距离的点,因为总共有n个点,1这个点的距离已经确定了,所以循环n-1次
    {
        int t = -1;//t=-1的作用是可以找出第一个点
        for (int j = 1; j <= n; j++)//遍历dist数组,找到没有确定最短路径的节点中距离源点最近的点t,//第一轮循环,寻找与起点最短距离的点
            if (!st[j] && (t == -1 || dist[t] > dist[j]))
                t = j;

        st[t] = true;//state[i] 置为 1(true)。
        for (int j = 1; j <= n; j++)
            dist[j] = min(dist[j], dist[t] + g[t][j]);//更新距离
    }
    if (dist[n] == 0x3f3f3f) return -1;//如果dist[n]被更新了,则存在路径
    return dist[n];
}
int main()
{
    scanf("%d %d", &n, &m);
    //初始化临界矩阵
    memset(g, 0x3f, sizeof g);
    while (m--)
    {
        int a, b, c;
        scanf("%d %d %d", &a, &b, &c);//输入a-->b的边长c
        g[a][b] = min(g[a][b], c);//a到b之间最短的边
    }
    int t = dijkstr();
    printf("%d\n", t);
    return 0;
}

2.用堆优化的Dijkstra算法

        总体逻辑与上述代码相似,只是通过优先队列来实现。

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<cstring>
#include<algorithm>
#include<queue>
using namespace std;
const int N = 100010;
bool st[N];// 如果为true说明这个点的最短路径已经确定
int h[N], e[N], ne[N], idx = 0;
//h[N]表示头节点的下标;e[i]表示表示节点i储存的值;ne[i]表示节点[i]的next指针为多少。
//idx表示储存到了那个值
int w[N]; //用来存权重
int dist[N];//储存各点到起点的距离
int n, m;
typedef pair <int, int> PII;堆里存储 距离 和 节点 编号
void add(int a, int b, int c)//初始化邻接表
{
	///头插!!!!!!!
	w[idx] = c;// a -> b 的边长为 c
	e[idx] = b; //数值   
	
	ne[idx] = h[a];
	h[a] = idx;
	idx++;
}
int dijkstr()
{
	memset(dist, 0x3f, sizeof(dist));
	dist[1] = 0;
	///priority_queue<Type,Container,Function>
	//默认容器vector,默认比较方式:<,即大顶堆,队头元素最大。
	priority_queue<PII, vector<PII>, greater<PII>> heap;// 定义一个小根堆,对头元素最小
	heap.push({ 0,1 }); //插入距离和节点编号
	//pair排序时是先根据first,再根据second,所以0,1位置不能互换
	while(heap.size())
	{
		auto t = heap.top(); //取距离源点最近的点
		heap.pop();

		int ver = t.second, distance = t.first;//ver:节点编号,distance:源点距离ver 的距离
		
		if (st[ver])  continue;//如果距离已经确定,则跳过该点
		st[ver] = true;

		for (int i = h[ver]; i != -1; i = ne[i])//从ver节点开始遍历/,更新ver所指向的节点距离
		{
			int j = e[i];//储存边
			if (dist[j] > distance + w[i])//距离变小,则入堆
			{
				dist[j] = distance + w[i];
				heap.push({ dist[j],j });
			}
		}
	
	}
	if (dist[n] == 0x3f3f3f3f) return -1;
	return dist[n];
}
int main()
{
	memset(h, -1, sizeof h);//将表头初始化为-1;
	scanf("%d%d", &n, &m);
	while (m--)
	{
		int a, b, c;//从a到b的边长为c
		scanf("%d%d%d", &a, &b, &c);
		add(a, b, c);
	}
	int t = dijkstr();
	cout << t << endl;
	return 0;
}

二.bellman-ford算法和SPFA算法(适用于含有负边权的题目

给定一个 n个点 m条边的有向图,图中可能存在重边和自环, 边权可能为负数

请你求出从 1 号点到 n 号点的最多经过 k 条边的最短距离,如果无法从 1 号点走到 n号点,输出 impossible

注意:图中可能 存在负权回路 。

输入格式

第一行包含三个整数 n,m,k。

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

点的编号为 1∼n。

输出格式

输出一个整数,表示从 1 号点到 n号点的最多经过 k条边的最短距离

如果不存在满足条件的路径,则输出 impossible

注意:SPFA算法各方面优于该算法,但是在碰到限制了最短路径上边的长度时就只能用bellman_ford了。

1.bellman-ford算法

         Bellman_ford算法可以用于存在负权回路的题目,是因为其循环的次数是有限制的因此最终不会发生死循环。而SPFA算法则不可以,会陷入死循环。

//O(MN)   N为顶点数,M为边数

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 510, M = 100010;
int dist[N];
int back[N];//备份数组放置串联
int n, m, k;//k代表最短路径最多包涵k条边
struct EDGE
{
	int a, b, w;
}e[N];//储存每条边的起点,终点,权重

int bellman_ford()
{
	//初始化
	memset(dist, 0x3f, sizeof dist);
	dist[1] = 0;

	for (int i = 0; i < k; i++)//遍历k次
	{
		memcpy(back, dist, sizeof dist);//备份   back = disk,防止发生串联效应
		//串联指的是,在第k次迭代中途,dist[]中部分数据已经发生了迭代,得到的结果是k条边的最短路径
		//在后续中再次对其判断迭代后,得到了k+1条边时才能得到的距离,
		///如果没有要求最短距离至多k条边的话,则上述备份可省去,下面也可改为dist[a] + w;
		for (int j = 0; j < m; j++)//遍历每条边
		{
			int a = e[j].a, b = e[j].b, w = e[j].w;
			dist[b] = min(dist[b], back[a] + w);//注意不是dist[a] + w;
		}
	}
	if (dist[n] >= 0x3f3f3f3f / 2) return -1; // 因为有负权的值,可能略小于正无穷,所以不能用dist[n] == 0x3f3f3f3;
	else return dist[n];
}
int main()
{
	scanf("%d%d%d", &n, &m, &k);
	for (int i = 0; i < m; i++) {
		int a, b, w;
		scanf("%d%d%d", &a, &b, &w);
		e[i] = { a,b,w };
	}
	int t = bellman_ford();
	if (t == -1) puts("impossible");
	else cout << t;
	return 0;
}

2.SPFA 算法(与堆优化版的dijkstra算法相似)

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<cstring>
#include<algorithm>
#include<queue>
using namespace std;
const int N = 100010;
bool st[N];// 如果为true说明这个点的最短路径已经确定
int h[N], e[N], ne[N], idx = 0;
//h[N]表示头节点的下标;e[i]表示表示节点i储存的值;ne[i]表示节点[i]的next指针为多少。
//idx表示储存到了那个值
int w[N]; //用来存权重
int dist[N];//储存各点到起点的距离
int n, m;

void add(int a, int b, int c)//初始化邻接表
{
	///头插!!!!!!!
	w[idx] = c;// a -> b 的边长为 c
	e[idx] = b; //数值   

	ne[idx] = h[a];
	h[a] = idx;
	idx++;
}
int SPFA()
{
	memset(dist, 0x3f, sizeof(dist));
	dist[1] = 0;

	queue<int> q;//存储所有变小的结点
	q.push(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])//如果在队列内,则无需入队,如果没在则需优化,则入队
				{
					q.push(j);
					st[j] = true;
				}
			}
		}
	}

	if (dist[n] == 0x3f3f3f3f) return -1;
	return dist[n];
}
int main()
{
	memset(h, -1, sizeof h);//将表头初始化为-1;
	scanf("%d%d", &n, &m);
	while (m--)
	{
		int a, b, c;//从a到b的边长为c
		scanf("%d%d%d", &a, &b, &c);
		add(a, b, c);
	}
	int t = SPFA();
	if (t == -1) puts("impossible");
	else cout << t;
	return 0;
}

总结:Bellman-ford可以处理任意带负权边和负权环的图,SPFA可以处理带负权边的图,Dijkstra只能处理带正权边的图。

                                                                                                                                                                题目来源于ACWing

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值