单源最短路
单源最短路的意思就是一个起点,算出到其他点的最短路,这里介绍三种算法,bellman-ford,spfa和dijkstra
BF算法
算法思想
BF是bellman_ford的简称,算法的想法非常简单,进行|V|−1次操作,每次操作对所有的边松弛。
松弛可以形象的理解为更新值,比如有一条从u到v的边,如果u上的值加上边的长度小于v上的值,那么就更新v上的值。
为什么这样做是对的呢?我们可以回顾整个过程,第一次操作,我们一定能将起点出去的点中的某一个点更新为最小值(这个点以后不会被更新),关于这点可以用反证法证明,如果没有一个点是更新完毕的,那么从起点到这个点必然存在一条比起点到这个点的边更短的路径,与假设矛盾。故而每次我们至少能确定一个点,所以总共需要|V|−1次(起点不用更新)。
最开始的图,0是起点
初始化
第一次松弛更新了1,2,3三个点
最后全部更新好了
上述的专业解析是引用一篇博文的,自己敲了一下Bellman-Ford最短路的算法,并附上比较详细的注释,方便记忆和理解(图的数据依旧引用上文,方便测试):
#include<iostream>
#include<queue>
#include<vector>
using namespace std;
const int M = 1001;
const int Maxn = 1000001;
int d[M],V,n;//V为最大点的数值(也就是总点数-1,因为还包括0),n为有向边的总条数
struct edge {
int to;
int cost;
edge() {};//默认构造函数+重设构造函数赋值
edge(int a,int b) {
to = a;
cost = b;
}//相当于edge(int tt,int cc):to(tt),cost(cc){}
};
vector<edge>G[M];
void Bellman_Ford(int s) {
fill(d , d +M, Maxn);//将s到各个点的距离初始化(为寻找最短路,若一条边不存在,默认距离为无穷大)
d[s] = 0;//s到s自身的距离为0
for (int i = 0; i < V - 1; i++) { //总共最多需要更新V-1次(有V个点)
for (int u = 0; u < V; u++) {
for (int j = 0; j < G[u].size(); j++) {//G[u]里面存放的就是所有以u为起点的有向边edge(to,cost)
int u_to_distance = G[u][j].cost;//u_to_distance为以u为起点的这条边的长度
int nextpoint = G[u][j].to;//nextpoint为以u为起点的边的终点
if (u_to_distance + d[u] < d[nextpoint]) {
d[nextpoint] = u_to_distance + d[u];
}//if目标点s→nextpoint(不经过u)的距离大于s→u→nextpoint的距离,
//那么更新s→nextpoint(不经过u)的距离为s→u→nextpoint的距离(因为更短)
}
}
}
}
int main() {
int a, b, c;//设为每条边的起点、终点和长度
while (~scanf("%d%d", &V,&n)) {//<span style="font-family: Arial, Helvetica, sans-serif;">V为最大点的数值(也就是总点数-1,因为还包括0),n为有向边的总条数</span>
for (int i = 0; i < n; i++) {
scanf("%d%d%d", &a, &b, &c);
G[a].push_back(edge(b, c));
}
Bellman_Ford(1);//这个起始点可以修改测试
for (int i =0; i <=V; i++)
cout << d[i] << " ";
}
system("pasue");
}
复杂度分析
复杂度很明显是O(V∗E),在完全图的时候,会变成
O(V3)
输入输出如上,最大点为4,有0,1,2,3,4五个点,共有7条有向边
分别输入起点+终点+边长,得到从起始点1到各个点的最短路长度:
1→0 没有路,距离d=100001(默认无穷大)
1→1 d=0;1→2没有路 d=100001;1→3 最短路d=3;1→4最短路d=6.
接下来谈一下SPFA算法:
SPFA算法又叫做bellman-ford队列优化,至于为啥叫SPFA我也不知道。
算法思想
纵观整个BF算法,我们可以用宽度优先搜索来代替V-1次的暴力松弛,每次用队列头部的元素去更新其他点,如果将一个点更新成功,就将这个点加入队列,直到更新不动为止。
可以证明,这样做和BF本质上是一样的。但这样做有个好处,这个好处就在于我们可以记录一个点是否在队列中,如果这个点在队列中,那么我们知道这个点一定会被拿出来松弛,那么当这个点被更新的时候,我们就不用讲他加入队列了。如此,就大大改进了BF算法。
SPFA代码就不重新打了,因为可以直接在Bellman-Ford算法的基础上加上队列的运用,但是可以得到很大大的效率优化,代码及注释如下:
#include<iostream>
#include<queue>
#include<vector>
using namespace std;
const int M = 1001;
const int Maxn = 1000001;
int d[M],V,n;
queue<int>pp;//声明一个队列放入被更新的点,下一次更新就以这些点为基础对其它跟这个点有关联的边进行更新
//这里先不用优先队列,到后面更加优化的Dijkstra算法才用
bool inqueue[M];//用于判断队列中是否已经存在这个点
struct edge {
int to;
int cost;
edge() {};//默认构造函数+重设构造函数赋值
edge(int a,int b) {
to = a;
cost = b;
}//相当于edge(int tt,int cc):to(tt),cost(cc){}
};
vector<edge>G[M];
void SPFA(int s) {
while (!pp.empty()) {
pp.pop();
}//初始化队列,将里面的数据清空(规范)
fill(d , d +M, Maxn);//将s到各个点的距离初始化(为寻找最短路,若一条边不存在,默认距离为无穷大)
d[s] = 0;//s到s自身的距离为0
pp.push(s);//此时队列中只有一个点,就是起始点s
inqueue[s] = 1;
while (!pp.empty()) { //总共最多需要更新V-1次(有V个点)
int u = pp.front();
pp.pop(); //每次拿出一个点,用初始点s到改点的距离更新与该点有关联的其它边的距离,同时把该点移除队列
inqueue[u] = 0;//去标记
for (int j = 0; j < G[u].size(); j++) {//G[u]里面存放的就是所有以u为起点的有向边edge(to,cost)
int u_to_distance = G[u][j].cost;//u_to_distance为以u为起点的这条边的长度
int nextpoint = G[u][j].to;//nextpoint为以u为起点的边的终点
if (u_to_distance + d[u] < d[nextpoint]) {
d[nextpoint] = u_to_distance + d[u];
if (!inqueue[nextpoint]) {//没有在队列里面,可以放进去
pp.push(nextpoint);
inqueue[nextpoint] = 1;
}
}//if目标点s→nextpoint(不经过u)的距离大于s→u→nextpoint的距离,
//那么更新s→nextpoint(不经过u)的距离为s→u→nextpoint的距离(因为更短)
}
}
}
int main() {
int a, b, c;//设为每条边的起点、终点和长度
while (~scanf("%d%d", &V,&n)) {
for (int i = 0; i < n; i++) {
scanf("%d%d%d", &a, &b, &c);
G[a].push_back(edge(b, c));
}
SPFA(1);
for (int i =0; i <=V; i++)
cout << d[i] << " ";
}
system("pasue");
}
输出的答案是一样的。但复杂度远比Bellman-Ford降低许多。
算法复杂度
这个算法的复杂度是个迷,据传是O(k∗E),其中
k是个不大的常数。
接下来再谈一下 Dijkstra算法
算法思想
我们依旧用宽度优先搜索的角度思考BF算法,我们发现,如果我们每次都将最小的值从队列中拿出,那么拿出来的一定是已经更新好了的(同样可以用反证法证明),所以只要我们将队列变成堆,就能快速处理最短路了。
还是刚刚那个图,最开始将0号节点加入堆
现在从堆中拿出最小的数0,用他更新1,2,3,并把1,2,3加入堆
现在堆里面最小的数是2号节点,说明2号节点已经更新好了,那么用2号节点去更新其他节点
最短的都更新好了
Dijkstra的代码要多敲几次加深理解,有几点需要注意一下,看一下注释:
#include<iostream>
#include<queue>
#include<vector>
using namespace std;
const int M = 1001;
const int Maxn = 1000001;
int d[M],V,n;//V为最大点的数值(因为还包括0,所以总点数是V+1),n为有向边的总条数
struct edge {
int to;
int cost;
edge() {};//默认构造函数+重设构造函数赋值
edge(int a,int b) {
to = a;
cost = b;
}//相当于edge(int tt,int cc):to(tt),cost(cc){}
};
struct node {
int point;
int d; //node是用来储存 (更新完的点point+s到point的距离d)
node() {};
node(int a, int b) {
point = a;
d = b;
}
bool operator<(const node &a) const{
return d > a.d;//(比较运算符的重载,是优先队列原本系统默认的从大到小的排列顺序
}//变成从小到大的排序,方便直接用nn.top()直接取队首的元素,也就是最小的
};
vector<edge>G[M];
priority_queue<node>nn;//声明一个名为nn的优先队列
void Dijkstra(int s) {
fill(d , d +M, Maxn);//将s到各个点的距离初始化(为寻找最短路,若一条边不存在,默认距离为无穷大)
d[s] = 0;//s到s自身的距离为0
while (!nn.empty())
nn.pop();
nn.push(node(s, 0));//同样,将初始数据放进优先队列
while(!nn.empty()){
node Next = nn.top();
nn.pop();
int u = Next.point;
int dd = Next.d;
if (d[u] < dd)continue;
for (int j = 0; j < G[u].size(); j++) {//G[u]里面存放的就是所有以u为起点的有向边edge(to,cost)
int u_to_distance = G[u][j].cost;//u_to_distance为以u为起点的这条边的长度
int nextpoint = G[u][j].to;//nextpoint为以u为起点的边的终点
if (u_to_distance + d[u] < d[nextpoint]) {
d[nextpoint] = u_to_distance + d[u];
nn.push(node(nextpoint, d[nextpoint]));//把更新完的点和距离放进队列里
}//if目标点s→nextpoint(不经过u)的距离大于s→u→nextpoint的距离,
//那么更新s→nextpoint(不经过u)的距离为s→u→nextpoint的距离(因为更短)
}
}
}
int main() {
int a, b, c;//设为每条边的起点、终点和长度
while (~scanf("%d%d", &V,&n)) {
for (int i = 0; i < n; i++) {
scanf("%d%d%d", &a, &b, &c);
G[a].push_back(edge(b, c));
}
Dijkstra(1);//这个起始点可以修改测试
for (int i =0; i <=V; i++)
cout << d[i] << " ";
}
system("pasue");
}
算法复杂度
由于我们至少将所有边遍历一次,所以我们需要O(E)的时间,堆优化使得我们在搜索时将一个
V
的复杂度降到
logV,所以时间复杂度为
O(E+VlogV)
根据《挑战程序设计竞赛》上的内容,Dijkstra算法的复杂度是O(|E|*log|V|),
需要注意的一点是,在图中存在负边的情况下,Dijkstra算法无法正确求解问题,还是需要使用Bellman-Ford和它的优化SPFA算法。
接下来是 多源最短路 Floyd-Warshall算法,
由于这个算法比较简单,在此处就直接引用源码了,不再由自己敲了,注释应该不需要,理解一下就好
如果我们想知道任意两个点之间的最短路,这时候可以暴力跑n次单源最短路,也可以用floyd算法
Floyd算法
这个算法的特点就是非常好写
算法思想
Floyd算法运用了动态规划的思想,对于一个最短路径(u,v)而言,我们一定是从u到了k,再从k到v。所以我们只要知道了(u,k)和(k,v)的最短路,那么我们就能得到(u,v)的最短路。
所以暴力枚举就好
算法分析
这个算法呢?是用来求任意两点间的最短路问题。可以试着用DP来求解任意两点间的最短路问题。只使用顶点0~k和i,j的情况下,记i到j的最短路长度为d[k+1][i][j]。k=-1时,认为只使用i和j,所以d[0][i][j]=cost[i][j]。接下来让我们把只使用顶点0~k的问题归约到只使用0~k-1的问题上。
只使用0~k时,我们分i到j的最短路正好经过顶点k一次和完全不经过顶点k两种情况来讨论。不经过顶点k的情况下,d[k][i][j]=d[k-1][i][j]。通过顶点k的情况下,d[k][i][j]=d[k-1][i][k]+d[k-1][k][j]。合起来就得到了d[k][i][j]=min(d[k-1][i][j],d[k-1][i][k]+d[k-1][k][j])。这个DP也可以使用同一个数组,不断进行d[i][j]=min(d[i][j],d[i][k]+d[k][j])的更新来实现。
算法实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| #include <iostream>
#include <cstring>
#include <algorithm>
#define MAX_V 102
#define INF 1008611
using namespace std;
int d[MAX_V][MAX_V];
int V,E;
int main(){
cin>>V>>E;
for(int i=1;i<=V;i++)
for(int j=1;j<=V;j++)
d[i][j]=(i==j?0:INF);
while(E--){
int u,v,c;
cin>>u>>v>>c;
d[u][v]=c;
}
for(int k=1;k<=V;k++)
for(int i=1;i<=V;i++)
for(int j=1;j<=V;j++)
d[i][j]=min(d[i][k]+d[k][j],d[i][j]);
for(int i=1;i<=V;i++)
for(int j=1;j<=V;j++)
cout<<i<<" "<<j<<" "<<d[i][j]<<endl;
return 0;
}
|
算法复杂度
很明显,复杂度是
O(V3)
时间复杂度还是很高的,但如果复杂度在可以承受的范围之内,可以使用这个实现起来非常简单的算法。
个人觉得,Dijkstra是SPFA的进一步优化,把SPFA中依靠普通队列中的点全部出列更新的操作转换为依靠优先队列(从小到大的预设)的第一个点(即那条更新后的最短边)来进行更新的操作,可以证明最短边是一定所属那个点更新完毕后所拥有的,这样一来就可以省去很多不必要的点继续拓展更新,快速找到最短路。
而SPFA又是Bellman-Ford的进一步优化,直接将O(V^3)的复杂度降低为用队列后的O(k*E),这是很大的一个优化。通过判断是否是被更新过的那些点,继续以S到这个被更新的点的距离(也就是更新后的更短的一条路)为基础对其它与它关联的更长的边进行更新。
Dijkstra用heap(堆)和链式前向星优化后几乎是最快的,但缺点就是不能求有负权的最短路与判断负环。