最短路径算法
诞生
之前讲解了BFS&DFS基本场景与演化。
(ps:https://blog.csdn.net/weixin_42178241/article/details/12595659)
最短路的诞生是因为每条边有多了一个权值,不能用BFS,DFS按照层次去算了。
新概念
最短路径还是图论的一种,和BFS,DFS一样有三种建图方式,不过我们引入了3个新概念。
- 源点,从a->b的最短路,a为源点
- 边权,边的权值,u->v有一条边,那边权就是w(u,v),意义可以定义
- 松弛,主要用于扩展刚求出一点最短路径去扩展其他子节点的操作(后面会解释)
方法
最短路径的求法思路有4大种,代码优化实现总共5种
Dijkstra
单源最短路(只能算从一个源点开始,到其他节点的最短路)
思想
每个节点的状态分3种,已知最短路,求出短路不确定是否最短,未求
我们首先将所有点赋值无穷大,用dis[i]表示源点到i的最短路径,dis[s]=0(s源点)
从源点开始向外扩展没有向外扩展的节点,更新其他节点的最短路,分别后续扩展
最后dis的值就ok了。
完美图解
抖音号:向往星辰大海的white_hacker的视频中有
代码实现
无优化
void dijkstra(int s){
for(int i=1;i<=n;i++) dis[i]=inf; // dis用来存储源点到任意结点的最短路,初始值无穷大
dis[s]=0; // 源点到自身的最短路显然为 0
for(int i=1;i<n;i++){ // 每次扩展一个结点,除源点外共有n-1个结点待扩展
int u,cost=inf;
// 找到距离孤岛最近的结点
for(int j=1;j<=n;j++){
if(!vis[u]&&dis[j]<cost){
cost=dis[j];
u=j;
}
}
vis[u]=true; // u 标记为已访问
// 对 u 进行扩展
for(int j=head[u];j;j=edge[j].nxt){
int v=edge[j].v,w=edge[j].w;
if(!vis[v]&&dis[v]>dis[u]+w)dis[v]=dis[u]+w;
}
}
}
优先优化
优先优化 实际上与无优化的Dijkstra的思路一样,只是在每次扩展点的时,加以数据结构对时间复杂度的优化。
struct node{
int ds,op;
bool operator>(const node& a)const {return ds>a.ds;}
};
priority_queue< node,vector<node>,greater<node> >q;
struct edge{
int nxt,v;
ll w;
}a[maxm];
inline void add(int u,int v,ll w){
a[++cnt].v=v;
a[cnt].w=w;
a[cnt].nxt=head[u];
head[u]=cnt;
}
inline void dijkstra(){
dis[s]=0;
q.push({dis[s],s});
while(!q.empty()){
int x=q.top().op;
q.pop();
if(vis[x])continue;
vis[x]=true;
for(int i=head[x];i;i=a[i].nxt){
if(dis[a[i].v]>dis[x]+a[i].w){
dis[a[i].v]=(ll)dis[x]+a[i].w;
if(!vis[a[i].v])q.push({dis[a[i].v],a[i].v});
}
}
}
}
最短路径证明
每个节点只会经过一次扩展,每次扩展的都是在没有扩展过的且被标记过路径的最短,去扩展其他点,绝对性的保障了最短路径的最优解
Floyd
思想
动态规划,主要想看,i->j的最短路中间加一个k点,能否比之前路径短
代码实现
只有5行很简单
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]);
}
}
最短路证明
问题来了,每次加点只能保证是两条边的最短路,会不会不包含呢?
解答来了
{解答来了}
解答来了
包含,假设有两个点u,v,k,且u,v之间有一条边,这时我们开始加点了u->k,k->v,算在了d[u][v]里,之后继续调用它,得到证明。
Bellman-ford
思想
Bellman-ford算法,主要是在松弛边,共松弛n-1次,如果还能松弛,证明此图有环.
代码实现
inline bool Bellman_ford(){
//初始化
memset(dis,0,sizeof(dis));
dis[s]=v;
//n-1次松弛
for(int i=1;i<n;i++){
bool flag=false;
//找边松弛
for(int j=1;j<=cnt;j++){
//判断赋值
if(dis[e[j].b]<(dis[e[j].a]-e[j].c)*e[j].r){
dis[e[j].b]=(dis[e[j].a]-e[j].c)*e[j].r;
flag=true;
}
}
if(!flag)return false;
}
//还可以松弛 ==》(有环图)
for(int i=1;i<=cnt;i++){
if(dis[e[i].b]<(dis[e[i].a]-e[i].c)*e[i].r)return true;
}
return false;
}
最短路证明
与Dijkstra相反,每个点可以多次扩展,每次保证用最小值扩展其他子节点,可以保证最短路,还可以判断负环。
SPFA(Bellman-ford的队列优化)
思想
和Bellman-ford一样;
代码实现
bool spfa( int s ){
for ( int i = 1; i <= n; i++ )
dis[i] = inf; /* 初始化操作 */
queue<node> q;
dis[s]=0;
in[s]= true; /* 标记结点是否在队列中 */
cnt[s]++; /* 记录结点进入队列的次数 */
q.push( s );
while ( q.size() )
{
int u = q.front(); q.pop();
in[u] = false;
for ( int j = head[u]; j; j = edge[j].nxt )
{
int v = edge[j].v, w = edge[j].w;
if ( dis[v] > dis[u] + w ) /* 对相邻的每条边进行松弛 */
dis[v] = dis[u] + w;
if ( in[v] )
continue;
q.push( v );
in[v] = true;
cnt[v]++;
if ( cnt[v] > n ){
return(false); /* 存在负环 */
}
}
}
return(true);
}
总结
Dijkstra未优化
· 时间复杂度:O(n^2)
· 空间复杂度:O(m)
· 应用场景:单源最短路 边权均为正 无环图
Dijkstra堆优化
· 时间复杂度: O(m*log(m))
· 空间复杂度: O(m)
· 应用场景: 单源最短路 边权均为正 无环图
Floyd
· 时间复杂度:O(n^3)
· 空间复杂度:O(n^2)
· 应用场景:多源最短路 边权环都兼容
Bellman-ford
· 时间复杂度:O(n*m)
· 空间复杂度:O(m)
· 应用场景:单源最短路 边权环都兼容
SPFA
· 时间复杂度:O(n*m)
· 空间复杂度:O(m)
· 应用场景:单源最短路 边权环都兼容
后记
后面会讲解tarjan算法和A*算法,尽情期待