最短路
话说至今也好久都没做到最短路的题了,大比赛被思维题卡得死死的,都没机会做到算法题,
(甚至怀疑是不是图论现在不吃香了啊?)
图论思维题倒是有遇到过,像是1e9数据的图里给定点找相邻的点,也算不上啥。
正文开始吧。
最短路呢,根据题目要求和数据范围有几种经典算法。
首先呢,讲讲存图
方法有四种,邻接表,邻接矩阵,前向星,链式前向星。
这里,就只讲链式前向星了。why?因为最好用,也泛用。
话我就写在代码注释里吧,分开写也不好看着理解。(草稿文件夹里的代码终于得见天日了)
基本定义:

#define N 2021
int head[N],num;
/*
首先,num为边的编号,这个很简单理解
head数组要跟mp结构体里的next一起理解,head[x]表示起点为x的路径(一串边的编号的最后一个),
每次更迭都会将head[x]的值存入mp[num].next
然后遍历访问的时候,通过访问next来找上一边
*/
struct iii
{
int to;//指向下一点
int next;//配合head数组理解
int dis;//没有边权的时候可以不写
}mp[N];
边的增删操作,这是理解链式前向星的关键,没理解不用着急,
下面我准备了一组数据,大概容易到一看就懂的那种地步。
(结合基本定义来理解)
增:

void add(int x,int y,int z)
{
//增加x到y的边,权值为z
mp[++num].dis=z;
mp[num].to=y;
mp[num].next=head[x];
head[x]=num;
}
用一组数据来说明吧
5 5 1
1 2 10
2 3 1
4 5 10
3 4 10
1 3 20
5 5 1表示5个点,5条边,起点为1
理解的关键看1 2 10和1 3 20的两行即可,
从遍历的角度讲一遍就可以理解了:for(int i=head[x];i!=0;i=mp[i].next)
比如现在遍历起点为1的边,
那么首先进入循环时,i=head[1],可以看出i=5
然后此时会去找mp[5],然后通过next去找这个起点的上一条边
看出next=1,自然,i=mp[5].next,也就是i=1
就去找到了mp[1]
这就是链式前向星!
很自然的可以知道,链式前向星的遍历是添加边时候的逆序遍历。
删:

void del(int x,int y)
{
//删除x到y的边
int last=0;
for(int i=head[x];i;i=mp[i].next)
{
int z=mp[i].to;
if(y==z)
{
if(i==head[x])head[x]=mp[i].next;
else mp[last].next=mp[i].next;
break;
}
last=i;
}
}
其实懂了添加以后,删除操作也就简单了,
对应修改一下next和head数组的值即可
最后,借以dijkstra算法来理解链式前向星吧。

#include<cstdio>
#include<cstring>
#include<cmath>
#include<queue>
#include<algorithm>
using namespace std;
typedef long long ll;
#define N 1000009
#define INF 0x3f3f3f3f
/*
#define N 2021
int head[N],num;
/*
首先,num为边的编号,这个很简单理解
head数组要跟mp结构体里的next一起理解,head[x]表示起点为x的路径(一串边的编号的最后一个),
每次更迭都会将head[x]的值存入mp[num].next
然后遍历访问的时候,通过访问next来找上一边
*/
struct iii
{
int to;//指向下一点
int next;//配合head数组理解
int dis;//没有边权的时候可以不写
}mp[N];
int n,m,s,dis[N],vis[N],head[N],num;
int cnt=0;
struct iii
{
int to,next,dis;
}mp[N];
struct ii
{
int dis,pos;
bool operator <(const ii &x)const
{
return x.dis<dis;
}
};
priority_queue<ii>q;
void add(int x,int y,int z)
{
//增加x到y的边,权值为z
mp[++num].dis=z;
mp[num].to=y;
mp[num].next=head[x];
head[x]=num;
printf("mp[%d]:dis=%d, to=%d, next=%d, head[%d]=%d\n",num,z,y,mp[num].next,x,num);
}
void del(int x,int y)
{
//删除x到y的边
int last=0;
for(int i=head[x];i;i=mp[i].next)
{
int z=mp[i].to;
if(y==z)
{
if(i==head[x])head[x]=mp[i].next;
else mp[last].next=mp[i].next;
break;
}
last=i;
}
}
void Dijkstra()
{
printf("第%d次:\n",cnt++);
dis[s]=0;
q.push((ii){0,s});
while(q.size())
{
ii now=q.top();
q.pop();
int x=now.pos,y;
if(vis[x])continue;
vis[x]=1;
for(int i=head[x];i;i=mp[i].next)
{
y=mp[i].to;
printf("%d->mp[%d].to==%d\n",x,i,y);
if(dis[y]>dis[x]+mp[i].dis)
{
dis[y]=dis[x]+mp[i].dis;
if(!vis[y])
{
q.push((ii){dis[y],y});
}
}
}
}
while(q.size())q.pop();
}
int main()
{
/*
示例数据:
5 5 1
1 2 10
2 3 1
4 5 10
3 4 10
1 3 20
*/
num=0;
scanf("%d %d %d",&n,&m,&s);
for(int i=1;i<=n;++i)dis[i]=INF,vis[i]=0;
for(int i=1;i<=m;++i)
{
int x,y,z;
scanf("%d %d %d",&x,&y,&z);
add(x,y,z);
}
puts("\n原版:");
Dijkstra();
for(int i=1;i<=n;++i)
printf("%d ",dis[i]);
for(int i=1;i<=n;++i)dis[i]=INF,vis[i]=0;
puts("\n删2到3的边后:");
del(2,3);
Dijkstra();
for(int i=1;i<=n;++i)
printf("%d ",dis[i]);
//del(2,3);
for(int i=1;i<=n;++i)dis[i]=INF,vis[i]=0;
puts("\n删后又恢复性增加2到3的边后:");
add(2,3,1);
Dijkstra();
for(int i=1;i<=n;++i)
printf("%d ",dis[i]);
return 0;
}
退一步说,现在来说算法吧。
概括性的说大致也就几种经典算法,根据题意和数据范围来选择。
1.Floyd算法

for(int k=0;i<n;++k)
for(int i=0,i<n;++i)
for(int j=0;j<n;++j)
mp[i][j]=min(mp[i][j],mp[i][k]+mp[k][j]);

queue<int>q;
mt(in);//负权处理
q.push(s);
while(q.size())
{
int x=q.front(),y;
q.pop();
vis[x]=0;//还可以松弛操作
for(int i=head[x];i;i=mp[i].next)
{
y=mp[i].to;
if(dis[y]<dis[x]+mp[i].dis)
{
dis[y]=dis[x]+mp[i].dis;
if(!vis[y])
{
vis[y]=1;//已使用
q.push(y);
if(++in[y]>n)return ;
}
}
}
}
SPFA(Shortest Path Faster Algorithm)算法是求单源最短路径的一种算法,它是Bellman-ford的队列优化算法。
很多时候,给定的图存在负权边,这时类似Dijkstra等算法便没有了用武之地,而Bellman-Ford算法的复杂度又过高,SPFA算法便派上用场了。SPFA的复杂度大约是O(kE),k是每个点的平均进队次数(一般的,k是一个常数,在稀疏图中小于2)。
但是,SPFA算法稳定性较差,在稠密图中SPFA算法时间复杂度会退化。
另外,SPFA算法还可以判断图中是否有负权环,即一个点入队次数超过N。
3.Dijkstra算法

dis[s]=0;
q.push((ii){0,s});
while(q.size())
{
ii now=q.top();
q.pop();
int x=now.pos,y;
if(vis[x])continue;
vis[x]=1;
for(int i=head[x];i;i=mp[i].next)
{
y=mp[i].to;
printf("%d->mp[%d].to==%d\n",x,i,y);
if(dis[y]>dis[x]+mp[i].dis)
{
dis[y]=dis[x]+mp[i].dis;
if(!vis[y])
{
q.push((ii){dis[y],y});
}
}
}
}
Dijkstra 算法求的是正权图的单源最短路问题,对于权值有负数的情况就不能用 Dijkstra 求解了,因为如果图中存在负环,可以从起点走到负环处一直将权值变小,从而使得 Dijkstra 带优先队列优化的算法就会进入一个死循环。
其实,spfa和dijkstra算法都一样,只是vis处理相反,作用也就不太一样了。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 25岁的心里话
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 一起来玩mcp_server_sqlite,让AI帮你做增删改查!!
· 零经验选手,Compose 一天开发一款小游戏!