最短路径
最短路径算法思想包括 Floyd,Dijskra,Bellman-Ford三大类
学好算法思想,代码不就呈现在眼前了吗?
只需要代码的直接文末伺候
首先出场的是代码量最简洁[4行],但是复杂度最高[O(n^3)]的Floyd算法
Floyd
Floyd算法思想比较容易理解。
比如当我们在寻找两个节点之间最短距离的时候,最容易想到的就是列出所有可能的路径,然后选择路径最小值。
至于如何列出所有可能的路径,当然是一个一个增加节点的数量来列出路径,例如不经过任何一个节点 v0->vn,经过一个节点 v0 -> v1 ->vn,两个节点 v0 -> v1 -> v2 -> vn,直到n-1个节点 v0->v1->v2->…->vn。
当列出所有路径的权值时,就可以得到最短路径了。
对于上面的思想,我们容易理解,但是计算机不太好理解。我们需要转换一下,例如我们可以这么考虑,当考虑点 vi 到点 vj 的最短距离时:
假设edge[i][j] 记录点 i 到点 j 的距离。
(便于计算机循环,从点v0开始考虑)
经过一个节点[v0]时,判断 edge[i][j] 和 edge[i][0]+edge[0][j] 取最小值更新edge[i][j]路径信息
经过两个节点[v0,v1],判断 edge[i][j] 和 edge[i][1]+edge[1][j] 取最小值更新edge[i][j]路径信息
...
经过n-1个节点[v0,v1,...,vn-2],判断 edge[i][j] 和 edge[i][n-2]+edge[n-2][j] 取最小值更新edge[i][j]路径信息
到这里不免会有疑问,为什么增加节点数目,只考虑后面增加的节点情况呢。
这也是Floyd算法精妙的地方,它利用动态规划的方法,遍历每一个经过点v0的路径进行更新。举个例子,假如刚开始如下图:
Floyd算法会首先遍历搜索所有经过点0的路径进行更新,更新结束后的结果如下图:
这样的话就容易理解了!!
所以Floyd的算法最最最最重要的就是对每一个点,遍历所有经过这个点的路径进行更新,然后相当于删除这个点。
这样的话,代码就呈现在眼前了~~
for(int k = 0; k < n; ++k)
for(int i = 0; i < n; ++i)
for(int j = 0; j < n; ++j)
edge[i][j] = ((edge[i][j] > edge[i][k] + edge[k][j])?(edge[i][k] + edge[k][j]) : edge[i][j] );
其次向我们走来的Dijstra算法,它是处理单源点无负权的优先算法,算法复杂度[O(n^2)]
Dijskra
算法的原理是比较好理解的,举个例子可以更好的说明,如下:
还是用这个图举例,加入要计算出以 i 为源点到其它各点的最短距离
Round | 0 | 1 | j | { i } |
---|---|---|---|---|
1 | i->0 (6) | i->1 (1) | i->j (6) | { i,1 } |
2 | i->1->0 (2) | i->1->j (5) | { i,1,0 } | |
3 | i->1->0->j (4) | { i,1,0,j } |
算法的思想就是在最开始的路径中找出权值最小(与此点的最短路径)的点,然后更新路径,继续选择还没有收录点中的最小值(与此点的最短路径),更新路径,如此往复,直至把所有的点都收录。
算法思想倒是比较容易理解,但是代码却不如Floyd算法那么简洁,但其实只要我们把这个算法一个步骤一个步骤剖析出来,也就容易比较好写了。
首先,我们需要两个数组,一个数组记录点与点直接的最短距离,这个数组的原始值为源点到其它各个点的距离。另一个数组记录该点是否被收录。
接着就是在未收录的点中寻找最短路径、更新路径的循环中,直至所有点被收录(其实也就是n-1次)。
代码这不就出现了吗:
path[n];//记录源点到其他点的最短距离
flag[n];//记录结点是否被确认,1表示已被收录
//初始化 path 和 flag,假设源点为s
flag[n] = {0};
flag[s] = 1;
for(int i = 0; i < n; ++i) {
//edge[][] 记录点与点之间的距离
path[i] = edge[s][i];
}
//寻找n-1次
for(int i = 0; i < n-1; ++i) {
int shorter = max;
int node = 0;
//寻找当前最短的路径
for(int j = 0; j < n; ++j) {
if(!flag[j] && shorter > path[j]) {
shorter = path[j];
node = j;
}
}
//更新标志位
flag[node] = 1;
//更新路径
for(int j = 0; j < n; ++j) {
//只更新未被收录的点的路径,这存在一个动态规划的思想,可以看上面Floyd算法
if(!flag[j]) {
if(path[j] > path[node] + edge[node][j]) {
path[j] = path[node] + edge[node][j];
}
}
}
}
如果当出现负权的时候,使用Dijskra算法可能会失去作用,例如
根据Dijskra,则会出现错误的结果。所以为了解决单源点存在负权(必须无负环,否则不存在最短路径)图中的最短路径,就有了Bellman-Ford算法,时间复杂的为O(n*m)。
Bellman-Ford
其实如果了解时间复杂的话,我们就可以知道这个算法与边数(m)相关。
对于边,可以创建一个结构记录起点,终点以及权值方便处理。
struct Edge{
int start,end,weight;
};
Bellman-ford算法其实就是对起点不为空的边进行松弛优化,何为松弛,就是逐步缩小两点之间的距离。用数组dis[]记录从源点到节点的距离,初始值为Max,源点为0;
还是举例说明,就用上面的图吧,源点为0:
既然是对边进行循环迭代松弛,所以起始状态是独自分离的点,
dis[] = {0,Max,Max,Max};
遍历第一次后的情况为,
dis[] = {0,3,5,Max};
遍历第二次后的情况为,
dis[] = {0,3,5,6};
遍历第三次后的情况为,
dis[] = {0,3,3,6};
到此Bellman-ford算法就算是结束了。
下面看一下Bellman-ford算法的写法,相对于Dijskra,代码量是减少了一点,但是算法思想需要自己琢磨,其实也就是动态规划的问题。
for(int i = 0; i < n; ++i) {
for(int j = 0; j < m; ++j) {
if(dis[e[j].start] != M && dis[e[j].end] > dis[e[j].start] + e[j].weight) {
dis[e[j].end] = dis[e[j].start] + e[j].weight;
//如果迭代了n次 则存在负环
if(i == n-1) {
cout << "存在负环" << endl;
return false;
}
}
}
}
优化思想
1 设置flag当迭代时不再更新节点的时候,取消迭代;
2 设置队列来维护节点,只对松弛成功的节点进行迭代松弛,也就是SPFA算法
打印路径问题
上面的几个算法,都可以打印出最短路径数组。但是如果我们想要打印出路径的话,我们需要如何处理呢。
这里我们可以利用一个数组记录节点的直接前驱,当我们记录节点的前驱之后,则就可以用递归或者栈来实现路径打印了。
Floyd全部代码
#include <iostream>
#include <stack>
using namespace std;
int main() {
int max = 999999;
int n[5][5] = {0, 4, max, 2, max,
4, 0, 4, 1, max,
max, 4, 0, 1, 3,
2, 1, 1, 0, 7,
max, max, 3, 7, 0};
//path[][] 记录每个节点的直接前驱
int path[5][5];
for(int i = 0; i < 5; ++i) {
for(int j = 0; j < 5; ++j) {
cout << n[i][j] << "\t";
if(i != j && n[i][j] < max) path[i][j] = i;
else path[i][j] = -1;
}
cout << endl;
};
//floyd算法
for(int k = 0; k < 5; ++k) {
for(int i = 0; i < 5; ++i) {
for (int j = 0; j < 5; ++j) {
if(n[i][j] > n[i][k] + n[k][j]) {
n[i][j] = n[i][k] + n[k][j];
path[i][j] = path[k][j];
}
}
}
}
for(int i = 0; i < 5; ++i) {
for(int j = 0; j < 5; ++j) {
cout << n[i][j] << " ";
}
cout << endl;
};
for(int i = 0; i < 5; ++i) {
for(int j = 0; j < 5; ++j) {
cout << path[i][j] << " ";
}
cout << endl;
};
for(int i = 0; i < 5; ++i) {
for(int j = 0; j < 5; ++j) {
if(i == j) continue;
cout << "from "<< i << " to " << j <<" is " << n[i][j] << " : " << i;
stack<int> s;
s.push(j);
while(path[i][s.top()] != i) {
s.push(path[i][s.top()]);
}
while(!s.empty()) {
cout << "->" << s.top();
s.pop();
}
cout << endl;
}
}
return 0;
}
Dijskra全部代码
#include <iostream>
#include <stack>
using namespace std;
#define max 999999
void Dijskra(int* n, int start) {
int nodes[5][5];
for(int i = 0; i < 5; ++i) {
for(int j = 0; j < 5; ++j) {
nodes[i][j] = *n++;
}
};
int path[5], prenode[5];
bool flag[5] = {false};
flag[start] = true;
//初始化,path记录以start为源点到其他点的最短路径,flag记录节点是否找到,prenode记录节点前驱
for(int i = 0; i < 5; ++i) {
path[i] = nodes[start][i];
prenode[i] = start;
}
//count为节点数-1
for(int count = 0; count < 4; ++count) {
int shortpath = max;
int i = 0;
//寻找最短路径节点
for(int j = 0; j < 5; ++j) {
if(!flag[j] && path[j] < shortpath) {
shortpath = path[j];
i = j;
}
}
flag[i] = true;
//更新路径信息
for(int j = 0; j < 5; ++j) {
if(!flag[j]) {
if(path[j] > path[i] + nodes[i][j]) {
path[j] = path[i] + nodes[i][j];
prenode[j] = i;
}
}
}
}
for(int i = 0; i < 5; ++i)
cout << prenode[i] << "\t";
cout << endl;
for(int i = 0; i < 5; ++i) {
if(start == i) continue;
cout << "from " << start << " to " << i << " is "<< path[i] << " : " << start;
stack<int> s;
s.push(i);
while(prenode[s.top()] != start) {
s.push(prenode[s.top()]);
}
while(!s.empty()) {
cout << "->" << s.top();
s.pop();
}
cout << endl;
}
}
int main() {
int n[] = {0, 4, max, 2, max,
4, 0, 4, 1, max,
max, 4, 0, 1, 3,
2, 1, 1, 0, 7,
max, max, 3, 7, 0};
cout << "info :" << endl;
for(int i = 1; i < 26; ++i) {
cout << n[i-1] << "\t";
if(i % 5 == 0)cout << endl;
};
cout << "input your start node:" << endl;
int start;
while(cin >> start && start < 5) {
//传数组
Dijskra(&n[0], start);
}
return 0;
}
Bellman-Ford全部代码
#include <iostream>
#include <stack>
using namespace std;
#define M 999999
struct Edge{
int start;
int end;
int weight;
};
bool BellmanFord(Edge *e,int n, int m) {
for(int i = 0; i < m; ++i) {
cout << e[i].start << "->" << e[i].end << " " << e[i].weight << endl;
};
int begin;
cout << "please input begin node:" << endl;
cin >> begin;
//初始化dis
int dis[n], prenode[n];
for(int i = 0; i < n; ++i) {
dis[i] = M;
prenode[i] = i;
}
dis[begin] = 0;
//迭代松弛,正常情况下迭代n-1次
for(int i = 0; i < n; ++i) {
for(int j = 0; j < m; ++j) {
if(dis[e[j].start] != M && dis[e[j].end] > dis[e[j].start] + e[j].weight) {
dis[e[j].end] = dis[e[j].start] + e[j].weight;
prenode[e[j].end] = e[j].start;
//如果迭代了n次 则存在负环
if(i == n-1) {
cout << "存在负环" << endl;
return false;
}
}
}
}
for(int i = 0; i < n; ++i) {
cout << dis[i] << " ";
}
cout << endl;
for(int i = 0; i < n; ++i) {
cout << prenode[i] << " ";
}
cout << endl;
for(int i = 0; i < n; ++i) {
if (i == begin || i == prenode[i]) continue;
cout << "from " << begin << " to " << i << " is "<< dis[i] << " : " << begin;
stack<int> s;
s.push(i);
while(prenode[s.top()] != begin) {
s.push(prenode[s.top()]);
}
while(!s.empty()) {
cout << "->" << s.top();
s.pop();
}
cout << endl;
}
return true;
}
int main() {
int n,m;
cout << "please input nodes and edge number:" << endl;
cin >> n >> m;
Edge e[m];
for(int i = 0; i < m; ++i) {
cin >> e[i].start >> e[i].end >> e[i].weight;
}
BellmanFord(&e[0], n, m);
return 0;
}