目录
前言:
通过本章节的学习,能使你对图论中的最短路径有一定的认识。再通过一定的刷题联系,能够掌握基本的图论最短路径问题。
最短路问题分为:
1.单源最短路
1.1:所有边权都为正的最短路问题。
解决方案:
1.朴素版的Dijkstra算法。
2.堆优化版的Dijkstra算法。
1.2:存在负边权的最短路问题。
解决方案:
1.bellman-ford算法。
2.SPFA算法。
2.多元最短路
解决方案:
Floyd算法。
单源路最短路径:
朴素版的Dijkstra算法:
迪杰斯特拉算法(Dijkstra)是由荷兰计算机科学家狄克斯特拉于1959年提出的,因此又叫狄克斯特拉算法。是从一个顶点到其余各顶点的最短路径算法,解决的是有权图中最短路径问题。迪杰斯特拉算法主要特点是从起始点开始,采用贪心算法的策略,每次遍历到始点距离最近且未访问过的顶点的邻接节点,直到扩展到终点为止。
扩展的过程:找到始点距离最近且未访问过的顶点,用这个点去“松弛”其他的点。接下来我们用一道实例来看。
例题:
拿样例来模拟松弛的过程。
dis数组是1号点到其他点走k步的最短距离
题解:
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N=510;
int map[N][N],dis[N],m,n;
bool book[N];//用于标记,将松弛过的点标记一下。以后不再松弛这个点
int dijkstra()
{
memset(dis,0x3f,sizeof dis);
dis[1]=0;//自己到自己
for(int i=1;i<=n;i++)
{
int k=-1;
for(int j=1;j<=n;j++)
if(!book[j]&&(k==-1||dis[k]>dis[j]))
k=j;
book[k]=true;
for(int j=1;j<=n;j++)
dis[j]=min(dis[j],map[k][j]+dis[k]);
}
if(dis[n]==0x3f3f3f3f) return -1;
else return dis[n];
}
int main()
{
//先对邻接矩阵进行初始化
memset(map,0x3f,sizeof map);
cin>>n>>m;
while(m--)
{
int a,b,c;
cin>>a>>b>>c;
map[a][b]=min(c,map[a][b]);
}
cout<<dijkstra()<<endl;
return 0;
}
堆优化版的的Dijkstra算法:
其过程和朴素版的差不多,就是使用了一个堆,进行了优化,在实际写题中用到的不多。大多数都能用SPAF算法代替
代码:
#include<iostream>
#include<algorithm>
#include<cstring>
#include<queue>
using namespace std;
typedef pair<int,int> PII;
const int N=1000010;
int h[N],ne[N],e[N],w[N],dist[N],idx;//w表示权重
int n,m;
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 dijkstra()
{
memset(dist,0x3f,sizeof(dist));
priority_queue<PII,vector<PII>,greater<PII>> heap;
//小根堆的定义方式,PII的第一个变量存储的是距离,第二个变量存储的是该点的编号,内部按照第一个变量排序,即按距离排序
dist[1]=0;
heap.push({0,1});
while(heap.size())
{
auto t=heap.top();//选择最小的距离的点
heap.pop();//利用完该点之后要弹出
int ver=t.second;//表示该点的编号
if(st[ver]) continue;
/*
堆优化版的是将距离直接加入到堆中.
例如:dist[5]=9(在堆中{9,5},第一次更新时加入),dist[5]=7(在堆中{7,5},第二次更新时加入)
使用时用的是dist[5]=7,将该点({7,5})弹出后,在下一次循环中,如果{9,5}在堆顶的话,使用时
两者间肯定要选距离要小的那个,不能使用{9,5}重复更新,所以要用st数组进行标记
*/
st[ver]=true;//标记该点已经使用过了
for(int i=h[ver];i!=-1;i=ne[i])
{
int j=e[i];
if(dist[j]>dist[ver]+w[i])//如果j到起点的距离大于ver到1的距离加上ver到j的距离,就更新值的大小;
{
dist[j]=dist[ver]+w[i];
heap.push({dist[j],j});
/*
更新距离之后将该点的距离加入到堆中,这也是上述为何要进行标记的原因,
因为一个点的距离加入堆的次数可能有两次甚至更多,这样会影响到其他的点
例如:
{9,5},{7,5},{10,6},如果{7,5}被弹出后,堆中剩余的是{9,5},{10,6},堆顶
的元素是{9,5}而5这个点的距离已经被使用过了,所以要将{9,5}这个点忽视掉
*/
}
}
}
if(dist[n]==0x3f3f3f3f) return -1;
return dist[n];
}
int main()
{
memset(h,-1,sizeof(h));
cin>>n>>m;
while(m--)
{
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
add(a,b,c);
}
cout<<dijkstra()<<endl;
return 0;
}
bellman-ford算法:
贝尔曼-福特算法(英语:Bellman–Ford algorithm),求解单源最短路径问题的一种算法,由理查德·贝尔曼(Richard Bellman) 和莱斯特·福特创立的。有时候这种算法也被称为 Moore-Bellman-Ford 算法,因为Edward F. Moore也为这个算法的发展做出了贡献。它的原理是对图进行V-1次松弛操作,得到所有可能的最短路径。
这个算法可以求出走n步到达某个点的最短路径。(算法有两个循环构成)其外部循环有一个含义,就是循环几次,相当于走了几步。内部循环相当于是对走第n步的松弛。因为在执行这个算法的时候,我们只需要考虑边之间的关系即可,所以直接用一个结构体储存其边之间的关系即可。
这一题看起来和上面一题差不多,但是由于边权有负值,所以不能用Dijkstra算法来解决。
为什么不能用dijkstra算法?
dijkstra是基于贪心策略,每次都找一个距源点最近的点,然后将该距离定为这个点到源点的最短路径;但如果存在负权边,那就有可能先通过并不是距源点最近的一个次优点,再通过这个负权边,使得路径之和更小,这样就出现了错误。
题解:
#include <iostream>
#include <cstring>
using namespace std;//bellman - ford
const int N=1e4+10,M=510;
int st[M],back[M];
int m,n,k,number;
struct ff{
int x,y,z;
}map[N];
int bellman_ford()
{
memset(st,0x3f,sizeof st);
st[1]=0;
for(int i=1;i<=k;i++)//k是几,就相当于走了多少步
{
memcpy(back,st,sizeof st);
for(int j=0;j<number;j++)
{
int a=map[j].x,b=map[j].y,c=map[j].z;
st[b]=min(st[b],back[a]+c);
}
}
return st[m];
}
int main()
{
cin>>m>>n>>k;
number=n;
while(n--)
{
// cout<<n<<endl;
int a,b,c;
cin>>a>>b>>c;
map[n]={a,b,c};
}
if(bellman_ford()<0x3f3f3f3f/2)
cout<<bellman_ford()<<endl;
else
puts("impossible");
return 0;
}
SPAF算法:
其实SPAF算法就是优化版的bellman-ford算法。我们在学习benllman-ford算法的时候,会发现,在第二个循环中,会对每一个边都进行处理。而实际上,这里面有些边就没有发生过变化所以造成了时间上的浪费。我们这里可以用一个队列对bellman-ford算法进行一个优化,就是每次仅让最短路程发生变化的点(进队列)的相邻边进行“松弛操作”。这样就避免了很多比必要的过程。
题解:
#include <iostream>
#include <cstring>
#include <queue>
using namespace std;
//用邻接矩阵进行储存
const int N=1e5+10;
int head[N],ne[N],e[N],w[N],indx=0;//用邻接表进行储存
int m,n,st[N],book[N];
queue<int> q;
void add(int a,int b,int c)
{
w[indx]=c;e[indx]=b;ne[indx]=head[a];head[a]=indx++;
}
int spfa(int k)
{
memset(st,0x3f,sizeof st);
st[k]=0;
q.push(k);
book[k]=1;
while(!q.empty())
{
int s=q.front();
q.pop();
book[s]=0;
for(int i=head[s];i!=-1;i=ne[i])
{
int j=e[i];
if(st[j]>st[s]+w[i])
{
st[j]=st[s]+w[i];
if(book[j]==0)
{
q.push(j);
book[j]=1;
}
}
}
}
return st[n];
}
int main()
{
cin>>n>>m;
memset(ne,-1,sizeof ne);
memset(head,-1,sizeof head);
while(m--)
{
int x,y,z;
cin>>x>>y>>z;
add(x,y,z);
}
if(spfa(1)<0x3f3f3f3f)
cout<<spfa(1)<<endl;
else
puts("impossible") ;
return 0;
}
多元最短路:
Floyd算法:
又被称作是只有五行的算法。
基本思想就是:最开始只允许经过 1号顶点进行中转,接下来只允许经过1和2号顶点进行中转……允许经过 1~n 号所有顶点进行中转,求任意两点之间的最短路程。用一句话概括就是:从 i号顶点到j号顶点只经过前 k号点的最短路程。其实这是一种“动态规划”的思想。
例题:
题解:
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N=210;
int a[N][N];
int n,m,k;
int main()
{
cin>>n>>m>>k;
memset(a,0x3f,sizeof a);
for(int i=1;i<=n;i++)
a[i][i]=0;
while(m--)
{
int x,y,z;
cin>>x>>y>>z;
a[x][y]=min(a[x][y],z);//因为可能存在自环
}
for(int k=1;k<=n;k++)
{
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)
a[i][j]=min(a[i][j],a[i][k]+a[k][j]);
}
}//floyd算法的时间复杂度为O(n^2)
/* for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)
cout<<a[i][j]<<" ";
cout<<endl;
}*/
while(k--)
{
int x,y;
cin>>x>>y;
if(a[x][y]>=0x3f3f3f3f/2)
cout<<"impossible"<<endl;
else
cout<<a[x][y]<<endl;
}
return 0;
}
总结:
最短路径问题能够解决很多生活中的实际问题,所以掌握这几种常见的算法很重要!
近一周学习的总结!