想象一下,你住在一个布满错综复杂小道的魔法森林里,每条小道都连接着不同的神奇村落,而每个村落之间的小道上都有着不同长度的魔法光环,代表着你需要施放不同量的魔力才能穿越。现在,你想从家出发,去参加在森林另一端的一个盛大的魔法庆典,但你希望沿途耗费的魔力最少,这样到了庆典还能精力充沛地表演你的魔法绝技。
最短路
在这个故事里,森林就是一张“图”,村落是图中的“节点”或“顶点”,连接村落的小道则是“边”,而那些魔法光环的强度就是“边的权重”——可以是距离、时间、成本或是魔力消耗等任何需要你考虑的成本度量。
为了找到从家到庆典地点的“最短路”,你不想走冤枉路,也不想在某条特别耗魔的小道上浪费太多魔力。于是,你开始运用智慧,采用类似“迪杰斯特拉咒语”或“弗洛伊德巫术”这样的古老算法。这些魔法技巧能确保你一步步探索,每次选择都是当前看来最节省魔力的路径前进,最终绘制出整片森林中通往各个村落间魔力消耗最少的路线图。
迪杰斯特拉算法
让我们来一场奇幻旅程,想象我们身处一个神秘王国的地图之上,这个王国由许多城堡和蜿蜒的魔法道路相连,每条道路上都镌刻着前往下一个城堡所需的魔法能量值。我们的任务是,从智慧城堡出发,找到前往其他所有城堡所需的最省魔法能量的路线。这时,迪杰斯特拉公爵的古老秘法就派上了用场!
开启探险
第一幕:智慧之光 我们的旅程从智慧城堡(起始点)开始,迪杰斯特拉公爵告诉我们,首先标记智慧城堡为已访问,并且我们知道到达自己的位置不需要任何魔法能量,所以它的魔法成本是0。对于其他所有城堡,我们暂时不知道最佳路线,就像被迷雾笼罩,所以我们标记它们的魔法成本为无穷大,意味着尚未探索或未知。
for(int i=1;i<=n;i++)
{
dis[i]=inf;//先无穷远
}
dis[s]=0;//当前就是0
第二幕:光之扩散 接下来,公爵的秘法开始发挥作用。他让我们点亮一盏灯,这盏灯代表我们目前知道的最短路径所能达到的最远点。我们会在所有未访问的城堡中,找到离智慧城堡最近的那个,用我们的魔法能量去照亮它。这意味着我们会更新这个城堡的魔法成本为实际的最短路径值,并把它标记为已访问。
priority_queue<pair<ll,int> > q;
for(int i=1;i<=n;i++)
{
q.push(make_pair(-dis[i],i));
}
第三幕:逐步照亮 一旦点亮了一个新的城堡,我们就站在这个新位置,再次审视周围的未探索领域。利用这盏新灯,我们检查所有与之相邻、尚未访问的城堡,看看是否可以通过当前城堡,以更少的魔法能量到达它们。如果可以,我们就更新这些城堡的魔法成本,用我们的新发现替换之前的估计。
while(!q.empty())
{
int u=q.top().second;
q.pop();
if(vis[u]==1) continue;//找没走过的点
for(int i=0;i<G[u].size();i++)//遍历
{
int v=G[u][i].first;
int w=G[u][i].second;
//更新最短的值
if(dis[v]>dis[u]+w)
{
dis[v]=dis[u]+w;
q.push(make_pair(-dis[v],v));
}
}
vis[u]=1;
}
第四幕:循环往复 这个过程会不断重复,每一次我们都从已访问的城堡中挑选下一个最近的未知领域,然后更新路径信息,直到整个地图上的每一个城堡都被我们的灯光触及,没有未知的角落留下。这就意味着我们已经找到了从智慧城堡到每一个其他城堡的最省魔法能量的路线。
终章:智慧之路显形 当所有的城堡都被点亮,我们的旅程也达到了高潮。迪杰斯特拉公爵的秘法不仅为我们揭示了每一座城堡的直达路径,更重要的是,它教给我们一个宝贵的智慧:有时候,解决问题并不需要看到全局,只需一步步向前,每一步都做出当前的最佳选择,最终就能抵达最理想的彼岸。
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int N=1e5;
const int inf=1e9;
vector<pair<int,int> > G[N];
int n,m,s;
int vis[N],fa[N];
ll dis[N];//起点到点i的距离
void dij(int s)
{
for(int i=1;i<=n;i++)
{
dis[i]=inf;//先无穷远
}
dis[s]=0;//当前就是0
priority_queue<pair<ll,int> > q;
for(int i=1;i<=n;i++)
{
q.push(make_pair(-dis[i],i));
}
while(!q.empty())
{
int u=q.top().second;
q.pop();
if(vis[u]==1) continue;//找没走过的点
for(int i=0;i<G[u].size();i++)//遍历
{
int v=G[u][i].first;
int w=G[u][i].second;
//更新最短的值
if(dis[v]>dis[u]+w)
{
dis[v]=dis[u]+w;
q.push(make_pair(-dis[v],v));
}
}
vis[u]=1;
}
}
int main()
{
cin>>n>>m>>s;
for(int i=1;i<=m;i++)
{
int u,v,w;
cin>>u>>v>>w;
G[u].push_back(make_pair(v,w));
}
dij(s);
for(int i=1;i<=n;i++)
{
cout<<dis[i]<<" ";
}
return 0;
}
SPFA 算法
新纪元的开启 想象在同一个神秘王国中,迪杰斯特拉公爵的秘法已深入人心,但人们渴望更快、更高效地探索未知。这时,SPFA大师出现了,他提出了一种新的方法,利用队列这一神奇工具,让寻找最短路径的旅程变得更加迅速。
队列的魔法 不同于迪杰斯特拉公爵逐一点亮城堡的方式,SPFA大师使用了一个魔法队列queue。他首先将起始城堡(智慧城堡)放入队列中,每当一个城堡从队列中取出,就如同被选中的勇士,准备探索其周围的世界。
勇士的探索 取出的城堡会检查它所有未探索过的邻接城堡,看是否能通过当前城堡以更低的魔法能量到达。如果发现更优路径,就更新邻接城堡的魔法成本,并将这个邻接城堡加入队列,等待下一次探索。这个过程重复进行,直到队列为空,意味着所有可能的优化探索都已完成。
防环的智慧 为了防止无限循环,SPFA大师还赋予了每个城堡一个计数器,记录它被访问的次数。如果某个城堡被访问的次数超过了总城堡数,就认为存在负权环,意味着通过某种方式可以无限制地减少总魔法消耗,这是不合理的,此时算法会停止并报告错误。
终章:速度与激情 就这样,SPFA算法以其惊人的效率,在神秘王国的地图上飞速勾勒出一条条最省魔法的路径。它利用队列减少了很多不必要的检查,使得探索过程更加迅速,尤其是在处理某些特定类型的图时,相比迪杰斯特拉算法,它能够显著提高效率,展现出“更快”的魅力。
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define endl '\n'
const int inf=1e9;
ll n,m,s;
vector<pair<ll,ll> > g[100005];
ll dis[100005];
ll vis[100005];
void SPFA(ll s)
{
for(int i=1;i<=n;i++)
{
dis[i]=inf;
}
dis[s]=0;
queue<ll> q;
q.push(s);
vis[s]=1;
while(!q.empty())
{
int u=q.front();
q.pop();
vis[u]=0;
for(int i=0;i<g[u].size();i++)
{
int v=g[u][i].first;
int w=g[u][i].second;
if(dis[v]>dis[u]+w)
{
dis[v]=dis[u]+w;
if(vis[v]==0)
{
q.push(v);
vis[v]=1;
}
}
}
}
}
int main()
{
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin>>n>>m>>s;
while(m--)
{
ll u,v,w;
cin>>u>>v>>w;
g[u].push_back(make_pair(v,w));
}
SPFA(s);
for(int i=1;i<=n;i++)
{
if(dis[i]==inf)
{
cout<<(1<<31)-1<<" ";
}
else
{
cout<<dis[i]<<" ";
}
}
return 0;
}
Floyd算法
让我们进入一个更加宏伟的王国,这里拥有无数的城镇与错综复杂的道路网,构成了一个庞大的互联帝国。在这样的国度里,想要了解任意两座城镇之间最短的旅行路线,就需要借助一位传奇法师——弗洛伊德的秘传法术,即弗洛伊德算法(Floyd-Warshall Algorithm)。
弗洛伊德的魔法矩阵
弗洛伊德法师拥有一本巨大的魔法书,书页上记载着一个巨大的矩阵,这个矩阵代表了王国中所有城镇之间的直接道路及其魔法消耗(可以理解为距离或者成本)。矩阵中的每个格子都藏着两个城镇之间旅行的秘密,如果两个城镇直接相连,那么这个格子就写有具体的魔法值;如果两个城镇没有直接道路,那么就标记为“无穷大”,意味着需要通过其他地方中转。
时空穿梭的奥义
弗洛伊德算法的精髓在于,它通过一系列神奇的操作,让这个矩阵逐渐显露出所有城镇间最短路径的秘密。法师首先假设,如果要从城镇A到B,最开始只能考虑直接从A到B的路径。但是,随着法术的施展,他会考虑通过中间的其他城镇C来中转的可能性,也许这样绕路反而能更快到达,消耗的魔法更少。
每一次法术的循环,都相当于打开了一层新的时空通道,让原本看似遥远的两点之间,因为某个中转站的存在而变得触手可及。法师重复这个过程,直到考虑了通过王国中的每一个城镇作为中转的可能性,从而确保找到的每一对城镇之间的路径都是真正的最短。
智慧的结晶
整个过程中,弗洛伊德算法展现出了惊人的智慧和效率。虽然它的时间复杂度高达O(N^3),在大规模王国中施展可能会显得有些笨重,但对于中等规模或者特定类型的路径查询问题,它的全面性和准确性无可匹敌。它不仅仅给出了最短路径的长度,还能通过回溯法还原出实际的行走路线,就像是在复杂的王国地图上绘制了一张无所不包的最短路径网络图。
#include<bits/stdc++.h>
using namespace std;
#define ll long long
ll n,m;
ll u[5000],v[5000],w[5000];
int dp[5005][5005];
ll ans=0x7fffffff;
int main()
{
cin>>n>>m;
memset(dp,0x3f3f3f3f,sizeof(dp));
for(int i=1;i<=n;i++)
{
dp[i][i]=0;
}
while(m--)
{
int a,b,c;
cin>>a>>b>>c;
dp[a][b]=dp[b][a]=min(dp[a][b],c);
}
for(int k=1;k<=n;k++)
{
for(int v=1;v<=n;v++)
{
for(int w=1;w<=n;w++)
{
dp[v][w]=min(dp[v][w],dp[v][k]+dp[k][w]);
}
}
}
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)
{
if(dp[i][j]==0x3f3f3f3f)
{
dp[i][j]=0;
}
cout<<dp[i][j]<<" ";
}
cout<<endl;
}
return 0;
}
练习题
B3647 【模板】Floyd - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
P3371 【模板】单源最短路径(弱化版) - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
P4779 【模板】单源最短路径(标准版) - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
P1144 最短路计数 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
L2-001 紧急救援 - 团体程序设计天梯赛-练习集 (pintia.cn)
总结
迪杰斯特拉算法时间复杂度较低,是单源最短路,但不能在负权图中使用
SPFA可以在负权图使用,但复杂度稍微较高
floyd是多源最短路,但复杂度较高
百看不如一练,只有实践才是进步最快的方式,更要独立思考,如果想不出来了就看题解,会有眼前一亮的感觉。好啦,今天就到这里吧。下一期再见,记得给专栏点个关注,明天接着来哦~