Acwing算法基础课学习笔记(八)--搜索与图论之最短路问题-Dijkstra&&bellman-ford&&spfa&&Floyd

43 篇文章 2 订阅
10 篇文章 2 订阅
这篇博客详细介绍了图论中的最短路问题,包括Dijkstra算法的一般应用和有边数限制的情况,SPFA算法的优势及负环检测,以及Floyd算法解决多源汇最短路问题的动态规划方法。强调了图的建模和算法实现的重要性。
摘要由CSDN通过智能技术生成

最短路问题是一个比较大的问题,我们将花一节课的时间来介绍,这一节的内容其实也不算难,只是需要记忆的内容比较多,本节课所讲的几个模板都需要背熟练!代码就是要多写,形成肌肉记忆!一般来说,在做算法题的时候,抽象出图论的知识是比较容易的,小学数奥难度,核心还是在于算法实现,所以一定要多写!
在这里插入图片描述
图论问题的难点在于如何建图。

Dijkstra求最短路 I

在这里插入图片描述

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

using namespace std;

const int N = 510;
int n, m;
int g[N][N];//稠密图一般使用邻接矩阵
int dist[N];//记录每个节点距离起点的距离
bool st[N];//True表示已经确定最短路 属于s集合

int dijkstra()
{
	memset(dist, 0x3f, sizeof dist);//所有节点距离起点的距离初始化为无穷大
	dist[1] = 0; //起点距离自己的距离为零
	for (int i = 0; i < n; i++) // 迭代n次,每次可以确定一个点到起点的最短路
	{
		int t = -1;
		for (int j = 1; j <= n; j++)
			//不在s集合,并且如果没有更新过,则进行更新, 或者发现更短的路径,则进行更新
			if (!st[j] && (t == -1 || dist[t] > dist[j]))
				t = j;
		if (t == n)	break;

		st[t] = true;//加入到s集合中

		for (int j = 1; j <= n; j++)//找到了距离最小的点t,并用最小的点t去更新其他的点到起点的距离
			dist[j] = min(dist[j], dist[t] + g[t][j]);
	}

	if (0x3f3f3f3f == dist[n])	return -1;// 如果起点到达不了n号节点,则返回-1
	return dist[n]; //返回起点距离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);
		g[a][b] = min(g[a][b], c);//如果有重边,请保留权值最小的一条边
	}

	int t = dijkstra();
	printf("%d\n", t);

	return 0;
}

int t = -1;
//t的作用?
一开始t的赋值是-1
如果t没有被更新,
那么要更新一下t
其实t等于负多少都没事,因为在第一次 st[j] 成功时 就会直接将现在的 j 赋值给 t 。因此 t 只是起到一个 在前面的最短路径点集合确定好后 ,在开启新的一轮寻找最近点时,用来标记在该轮中是否已经心找到一个 (目前来说)最短点的作用。

// 0x3f 0x3f3f3f3f 的区别?
memset 按字节赋值,所以memset 0x3f 就等价与赋值为0x3f3f3f3f

Dijkstra求最短路 II

在这里插入图片描述

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

using namespace std;

typedef pair<int, int> PII;

const int N = 150010;

// 稀疏图用邻接表来存
int h[N], e[N], ne[N], idx;
int w[N]; // 用来存权重
int dist[N];
bool st[N]; // 如果为true说明这个点的最短路径已经确定

int n, m;

void add(int x, int y, int c)
{
    w[idx] = c; // 有重边也不要紧,假设1->2有权重为2和3的边,再遍历到点1的时候2号点的距离会更新两次放入堆中
    e[idx] = y; // 这样堆中会有很多冗余的点,但是在弹出的时候还是会弹出最小值2+x(x为之前确定的最短路径),并
    ne[idx] = h[x]; // 标记st为true,所以下一次弹出3+x会continue不会向下执行。
    h[x] = idx++;
}

int dijkstra()
{
    memset(dist, 0x3f, sizeof(dist));
    dist[0] = 1;
    priority_queue<PII, vector<PII>, greater<PII>> heap; // 定义一个小根堆
    // 这里heap中为什么要存pair呢,首先小根堆是根据距离来排的,所以有一个变量要是距离,其次在从堆中拿出来的时    
    // 候要知道知道这个点是哪个点,不然怎么更新邻接点呢?所以第二个变量要存点。
    heap.push({ 0, 1 }); // 这个顺序不能倒,pair排序时是先根据first,再根据second,这里显然要根据距离排序
    while (heap.size())
    {
        PII k = heap.top(); // 取不在集合S中距离最短的点
        heap.pop();
        int ver = k.second, distance = k.first;

        if (st[ver]) continue;
        st[ver] = true;

        for (int i = h[ver]; i != -1; i = ne[i])
        {
            int j = e[i]; // i只是个下标,e中在存的是i这个下标对应的点。
            if (dist[j] > distance + w[i])
            {
                dist[j] = distance + w[i];
                heap.push({ dist[j], j });
            }
        }
    }
    if (0x3f3f3f3f == dist[n]) return -1;
    else return dist[n];
}

int main()
{
    memset(h, -1, sizeof(h));
    scanf("%d%d", &n, &m);

    while (m--)
    {
        int x, y, c;
        scanf("%d%d%d", &x, &y, &c);
        add(x, y, c);
    }

    cout << dijkstra() << endl;

    return 0;
}

有边数限制的最短路

推荐题解
在这里插入图片描述

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

using namespace std;

const int N = 510, M = 10010;
int n, m, k;
int dist[N];//距离
int backup[N];//备份数组放置串联

struct Edge
{
	int a, b, w;
}edges[M];//把每个边保存下来即可

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

	for (int i = 0; i < k; i++)//k次循环
	{
		memcpy(backup, dist, sizeof dist);
		for (int j = 0; j < m; j++)//遍历所有边
		{
			int a = edges[j].a, b = edges[j].b, w = edges[j].w;
			dist[b] = min(dist[b], backup[a] + w);//使用backup:避免给a更新后立马更新b, 这样b一次性最短路径就多了两条边出来
		}
	}

	if (dist[n] > 0x3f3f3f3f / 2)	return -1;
	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);
		edges[i] = { a, b, w };
	}

	int t = bellman_ford();
	if (-1 == t)	puts("impossible");
	else printf("%d\n", t);
	return 0;
}

Bellman_ford算法一般没有spfa算法优,一般来说,只有存在最多经过k条边这个限制的时候,才会使用Bellman_ford 算法,其他存在负权边的情况下,我们都会使用下面的SPFA算法。

spfa求最短路

spfa是限制最少的一种最短路算法,也是对Bellman_ford算法的优化,只要图中没有负环即可,99.9%的最短路问题是没有负环的。可见spfa的强大之处。不过也有spfa算法已死的说法,都是坏得很的出题人的恶意(逃
如果被卡的话,就要换成堆优化版的dijsktra算法去做。

在这里插入图片描述
推荐题解

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

using namespace std;

const int N = 100010;

int n, m;
int h[N], w[N], e[N], ne[N], idx;
int dist[N];
bool st[N];

void add(int a, int b, int c)
{
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = 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被标记为false,代表之后该节点如果发生更新可再次入队
        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不在队列里才需要加入队列
                {
                    q.push(j);
                    st[j] = true;
                }
            }
        }
    }

    if (0x3f3f3f == dist[n])	return -1;
    return dist[n];
}

int main()
{
    scanf("%d%d", &n, &m);

    memset(h, -1, sizeof h);

    while (m--)
    {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        add(a, b, c);
    }

    int t = spfa();

    if (0x3f3f3f3f == t) puts("impossible");
    else printf("%d\n", t);

    return 0;
}

spfa判断负环

在这里插入图片描述

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

using namespace std;

const int N = 100010;

int n, m;
int h[N], w[N], e[N], ne[N], idx;
int dist[N];
int cnt[N];//cnt数组表示到达当前这个点最短路的边数
bool st[N];

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

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被标记为false,代表之后该节点如果发生更新可再次入队
        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;
                //经过n条边则经过了n+1个点,根据抽屉原理,说明经过某个点两次,则说明有环
                if (cnt[j] >= n) return true;

                if (!st[j])//当前已经加入队列的结点,无需再次加入队列,j不在队列里才需要加入队列
                {
                    q.push(j);
                    st[j] = true;
                }
            }
        }
    }
    
    return false;
}

int main()
{
    scanf("%d%d", &n, &m);

    memset(h, -1, sizeof h);

    while (m--)
    {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        add(a, b, c);
    }

    if (spfa()) puts("Yes");
    else printf("No");

    return 0;
}

Floyd求最短路

Floyd超简单,用来解决多源汇最短路问题,原理基于动态规划。经过三重循环就可以把邻接矩阵变成所要求的最短路。
在这里插入图片描述

d[k, i, j] = d[k - 1, i, k] + d[k - 1, k, i]

由于k不影响最后结果,因此可以去掉,所以最终的转移方程就是:

d[i, j] = d[i, k] + d[k, j]

完整代码如下:

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

using namespace std;

const int N = 210, INF = 1e9;
int n, m, Q;
int d[N][N];

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]);
}

int main()
{
	scanf("%d%d%d", &n, &m, &Q);

	for (int i = 1; i <= n; i++)
		for (int j = 1; j <= n; j++)
			if (i == j)	d[i][i] == 0;//自己到自己的距离是0
			else d[i][j] = INF;

	while (m--)
	{
		int a, b, w;
		scanf("%d%d%d", &a, &b, &w);
		d[a][b] = min(d[a][b], w);//有多条边的话只保留最小的边
	}

	floyd();

	while (Q--)
	{
		int a, b;
		scanf("%d%d", &a, &b);

		int t = d[a][b];
		if (t > INF / 2) puts("impossible");
		else printf("%d\n", t);
	}

	return 0;
}

本章总结:

Dijkstra-朴素O(n^2)

 1. 初始化距离数组, dist[1] = 0, dist[i] = inf;
 2. for n次循环 每次循环确定一个min加入S集合中,n次之后就得出所有的最短距离
 3. 将不在S中dist_min的点->t
 4. t->S加入最短路集合
 5. 用t更新到其他点的距离

Dijkstra-堆优化O(mlogm)

 1. 利用邻接表,优先队列
 2. 在priority_queue[HTML_REMOVED], greater[HTML_REMOVED] > heap;中将返回堆顶
 3. 利用堆顶来更新其他点,并加入堆中类似宽搜

Bellman_fordO(nm)

 1. 注意连锁想象需要备份, struct Edge{inta,b,c} Edge[M];
 2. 初始化dist, 松弛dist[x.b] = min(dist[x.b], backup[x.a]+x.w);
 3. 松弛k次,每次访问m条边

Spfa O(n)~O(nm)

 1. 利用队列优化仅加入修改过的地方
 2. for n次
 3. for 所有边利用宽搜模型去优化bellman_ford算法
 4. 更新队列中当前点的所有出边

Floyd O(n^3)

 1. 初始化d
 2. k, i, j 去更新d。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值