最短路问题

关于最短路有很多很多的算法,针对于不同的情境,点边范围,有很多算法思路。大致如下图:

在记忆的时候使用此图,可以跟好的理解,不会记得特别的乱。

一:朴素Dijkstra算法

朴素Dijkstra算法的时间复杂度为o(n^2),主要是用于单源最短路权值都为正的稠密图中。

首先给定起始点和终点,求起始点到终点的最短路径。
Dijkstra算法的做法是:

  1. 在所有顶点里找到距离起点最近的点,将它放入集合S。
  2. 用这个顶点来更新其它顶点到起点的距离。
  3. 重复1,2步,直到所有顶点都在集合S里,此时,终点存的距离就是终点到起点的最短距离。

模板题目

 先观察题目,很明显n远远小于m,所以用稠密图,其次,题目说可能存在自环和重边,如果是自环的话,因为题目说了边权都为正,所以至少这道题,不会出现自环,重边的话,因为我们求的是最短路,因此我们在输入的时候min一下,保留最小边权值的那个边就好了。

代码如下:

#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;

    //迭代n次,每次可以确定一个点到起点的最短路
    for (int i = 0; i < n; ++i) {
        int t = -1;
        //t的作用?

        for (int j = 1; j <= n; ++j) {
            //不在s集合,并且
            //如果没有更新过,则进行更新, 或者发现更短的路径,则进行更新
            if (!st[j] && (t == -1 || dist[j] < dist[t])) {
                t = j;
            }//这个地方为什么这样写涉及到数学的证明,不必太过于拘泥于这个地方如何实现。
        }

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

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

    // 如果起点到达不了n号节点,则返回-1
    if (dist[n] == 0x3f3f3f3f) return -1;
    // 返回起点距离n号节点的最短距离
    return dist[n];
}

int main() {
    cin >> n >> m;
    //所有节点之间的距离初始化为无穷大
    memset(g, 0x3f, sizeof g);
    // 0x3f 0x3f3f3f3f 的区别?

    while (m--) {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        g[a][b] = min(g[a][b], c); //如果有重边,请保留权值最小的一条边
    }

    cout << dijkstra() << endl;

    return 0;
}

二:堆优化版的Dijkstra算法

此算法用于边和点的数量级一致的时候大概就是10的5次方级别的时候,用优先队列去优化,以便降低时间复杂度到o(mlogn)。这种情况下,是稀疏图,要用到邻接表去存图。

模板题目

 看得出必须是优化版的才能过所有数据了,代码模板如下:

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

using namespace std;

typedef pair<int, int> PII;

const int N = 100010; // 把N改为150010就能ac

// 稀疏图用邻接表来存
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[1] = 0;
    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(dist[n] == 0x3f3f3f3f) 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;
}

三:Bellman-Ford算

1.什么是Bellman - Ford算法?
Bellman - ford 算法是求含负权图的单源最短路径的一种算法,效率较低,代码难度较小。其原理为连续进行松弛,在每次松弛时把每条边都更新一下,若在 n-1 次松弛后还能更新,则说明图中有负环,因此无法得出结果,否则就完成。
(通俗的来讲就是:假设 1 号点到 n 号点是可达的,每一个点同时向指向的方向出发,更新相邻的点的最短距离,通过循环 n-1 次操作,若图中不存在负环,则 1 号点一定会到达 n 号点,若图中存在负环,则在 n-1 次松弛后一定还会更新)

2.bellman - ford算法的具体步骤
for n次
for 所有边 a,b,w (松弛操作)
dist[b] = min(dist[b],back[a] + w)

注意:back[] 数组是上一次迭代后 dist[] 数组的备份,由于是每个点同时向外出发,因此需要对 dist[] 数组进行备份,若不进行备份会因此发生串联效应,影响到下一个点

3、在下面代码中,是否能到达n号点的判断中需要进行if(dist[n] > INF/2)判断,而并非是if(dist[n] == INF)判断,原因是INF是一个确定的值,并非真正的无穷大,会随着其他数值而受到影响,dist[n]大于某个与INF相同数量级的数即可
4、Bellman - Ford算法擅长解决有边数限制的最短路问题
时间复杂度 O(nm)  

模板题目

代码:这道题目前我还是不理解,这个算法还不会用

AcWing 853. 有边数限制的最短路 - AcWing  这个大佬讲的很不错。

#include<bits/stdc++.h>
using namespace std;
const int N=100010;
int dist[N],backup[N];
int k,n,m;
struct edge{
    int a;int b;int w;
}edge[N];
int bellman_ford()
{
    memset(dist,0x3f,sizeof dist);
    dist[1]=0;
    for(int i=1;i<=k;i++)
    {
        memcpy(backup,dist,sizeof dist);
        for(int j=1;j<=m;j++)
        {
            int a=edge[j].a,b=edge[j].b,w=edge[j].w;
            dist[b]=min(dist[b],backup[a]+w);
        }
    }
    return dist[n];
}
int main()
{
    cin>>n>>m>>k;
    for(int i=1;i<=m;i++)
    {
        int a,b,c;
        cin>>a>>b>>c;
        edge[i].a=a,edge[i].b=b,edge[i].w=c;
    }
    int t=bellman_ford();
    if(t>=0x3f3f3f3f/2)puts("impossible");
    else cout<<t<<endl;
}

 四:SPFA算法

AcWing 851. SPFA算法 - AcWing  解析看这个

此算法其实是上一个算法的优化,在求解问题上很方便,有些时候可以用它解决Dijkstra算法的问题,时间复杂度为线性,一般为o(m),最坏的时候为o(nm),并且可以判断是否存在负环。

题目:

代码: 

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

using namespace std;
const int N=100010;
int n,m;
int h[N],e[N],ne[N],w[N],idx;
int dist[N];
int 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;//起点到自己的距离为0 
	queue<int> q;
	q.push(1);//把起点加入队列 
	st[1]=1;//因为加入了队列,所以标记一下 
	
	while(!q.empty())
	{
		int t=q.front();
		q.pop();
		
		st[t]=0;//出来了,那就不在队列里了,就为0 
		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]=1;//进队要标记 
				}
			}
		}
	 } 
	 
	 if(dist[n]==0x3f3f3f3f) return -1;
	 return dist[n];
}

int main()
{
	cin>>n>>m;
	memset(h,-1,sizeof(h));//初始化链表头 
	for(int i=1;i<=m;i++)
	{
		int x,y,z;
		cin>>x>>y>>z;
		add(x,y,z); 
	}
	
	int res=spfa();
	if(res==-1)  cout<<"impossible"<<endl;
	else cout<<res<<endl;
	return 0;
}

五:多源汇最短路--Floyd算法

求解多个点到其他点的最短路问题。

其实floyd很简单,如果时间复杂度允许的情况下简直就不要太爽,但是无奈时间复杂度为o(n^3),有点不太友好。

AcWing 854. Floyd求最短路 - AcWing   解析看这个。

题目:

 

#include <iostream>
using namespace std;

const int N = 210, M = 2e+10, INF = 1e9;

int n, m, k, x, y, z;
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() {
    cin >> n >> m >> k;
    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;
    while(m--) {
        cin >> x >> y >> z;
        d[x][y] = min(d[x][y], z);
        //注意保存最小的边
    }
    floyd();
    while(k--) {
        cin >> x >> y;
        if(d[x][y] > INF/2) puts("impossible");
        //由于有负权边存在所以约大过INF/2也很合理
        else cout << d[x][y] << endl;
    }
    return 0;
}

完结!

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
以下是使用Matlab解决最短路问题的示例代码: ```matlab % 首先定义图的邻接矩阵 % 例如下面的邻接矩阵表示一个6个节点的有向图 % 从1到2的边权重为2,从1到3的边权重为4,以此类推 % 如果两个节点之间没有边相连,则边权重为inf G = [0 2 4 inf inf inf; inf 0 1 5 inf inf; inf inf 0 1 inf inf; inf inf inf 0 3 inf; inf inf inf inf 0 2; inf inf inf inf inf 0]; % 使用Dijkstra算法计算从节点1到其他节点的最短路径和距离 [start_node, dist] = dijkstra(G, 1); % 打印结果 fprintf('从节点1到其他节点的最短路径和距离如下:\n'); for i = 1:length(dist) fprintf('从节点1到节点%d的最短路径为:', i); print_path(start_node, i); fprintf(',距离为:%d\n', dist(i)); end % Dijkstra算法实现 function [start_node, dist] = dijkstra(G, s) n = size(G, 1); start_node = zeros(1, n); dist = inf(1, n); visited = false(1, n); dist(s) = 0; for i = 1:n-1 u = find_min_dist(dist, visited); visited(u) = true; for v = 1:n if ~visited(v) && G(u,v) ~= inf && dist(u) + G(u,v) < dist(v) dist(v) = dist(u) + G(u,v); start_node(v) = u; end end end end % 辅助函数:找到距离源节点最近的未访问节点 function u = find_min_dist(dist, visited) dist(visited) = inf; [~, u] = min(dist); end % 辅助函数:打印路径 function print_path(start_node, v) if start_node(v) == 0 fprintf('%d', v); else print_path(start_node, start_node(v)); fprintf(' -> %d', v); end end ``` 希望这个示例代码能够帮助您解决最短路问题

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值